mirror of
https://github.com/lldap/lldap.git
synced 2026-06-18 20:28:21 +00:00
Compare commits
38 Commits
copilot/fi
...
v0.6.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48a0a8d961 | ||
|
|
49dc766184 | ||
|
|
2b3dbb46de | ||
|
|
40121b80b7 | ||
|
|
b8465212b5 | ||
|
|
bb2ea7bf36 | ||
|
|
9fb252759a | ||
|
|
3a26d2ec4c | ||
|
|
86d9ea10d6 | ||
|
|
2ad634deda | ||
|
|
155bda6bbf | ||
|
|
7d1593e266 | ||
|
|
8c8df11250 | ||
|
|
aa1384939b | ||
|
|
6f94134fdc | ||
|
|
d1904a2759 | ||
|
|
02d92c3261 | ||
|
|
48058540ec | ||
|
|
618e3f3062 | ||
|
|
cafd3732f0 | ||
|
|
8588d4b851 | ||
|
|
2f70e2e31f | ||
|
|
a9d04b6bdf | ||
|
|
c03f3b5498 | ||
|
|
ac55dfedc4 | ||
|
|
62ae1d73fa | ||
|
|
469f35c12c | ||
|
|
ee9fec71a5 | ||
|
|
9cbb0c99e2 | ||
|
|
81e985df48 | ||
|
|
a136a68bf4 | ||
|
|
8f0022a9f1 | ||
|
|
fc7b33e4b3 | ||
|
|
a9b5147a30 | ||
|
|
4de069452f | ||
|
|
e5c28a61d9 | ||
|
|
c5e0441cae | ||
|
|
a959a50e07 |
38
.github/workflows/docker-build-static.yml
vendored
38
.github/workflows/docker-build-static.yml
vendored
@@ -87,14 +87,14 @@ jobs:
|
||||
image: lldap/rust-dev:latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5.0.0
|
||||
uses: actions/checkout@v6.0.2
|
||||
- name: Install Rust
|
||||
id: toolchain
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: "${{ env.MSRV }}"
|
||||
targets: "wasm32-unknown-unknown"
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
/usr/local/cargo/bin
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
restore-keys: |
|
||||
lldap-ui-
|
||||
- name: Install wasm-pack with cargo
|
||||
run: cargo install wasm-pack || true
|
||||
run: cargo install --locked wasm-pack || true
|
||||
env:
|
||||
RUSTFLAGS: ""
|
||||
- name: Build frontend
|
||||
@@ -114,7 +114,7 @@ jobs:
|
||||
- name: Check build path
|
||||
run: ls -al app/
|
||||
- name: Upload ui artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ui
|
||||
path: app/
|
||||
@@ -136,14 +136,14 @@ jobs:
|
||||
CARGO_HOME: ${GITHUB_WORKSPACE}/.cargo
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5.0.0
|
||||
uses: actions/checkout@v6.0.2
|
||||
- name: Install Rust
|
||||
id: toolchain
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: "${{ env.MSRV }}"
|
||||
targets: "${{ matrix.target }}"
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
.cargo/bin
|
||||
@@ -159,17 +159,17 @@ jobs:
|
||||
- name: Check path
|
||||
run: ls -al target/release
|
||||
- name: Upload ${{ matrix.target}} lldap artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ matrix.target}}-lldap-bin
|
||||
path: target/${{ matrix.target }}/release/lldap
|
||||
- name: Upload ${{ matrix.target }} migration tool artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ matrix.target }}-lldap_migration_tool-bin
|
||||
path: target/${{ matrix.target }}/release/lldap_migration_tool
|
||||
- name: Upload ${{ matrix.target }} password tool artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ matrix.target }}-lldap_set_password-bin
|
||||
path: target/${{ matrix.target }}/release/lldap_set_password
|
||||
@@ -209,7 +209,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: x86_64-unknown-linux-musl-lldap-bin
|
||||
path: bin/
|
||||
@@ -310,18 +310,18 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout scripts
|
||||
uses: actions/checkout@v5.0.0
|
||||
uses: actions/checkout@v6.0.2
|
||||
with:
|
||||
sparse-checkout: 'scripts'
|
||||
|
||||
- name: Download LLDAP artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: x86_64-unknown-linux-musl-lldap-bin
|
||||
path: bin/
|
||||
|
||||
- name: Download LLDAP set password
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: x86_64-unknown-linux-musl-lldap_set_password-bin
|
||||
path: bin/
|
||||
@@ -506,21 +506,21 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5.0.0
|
||||
uses: actions/checkout@v6.0.2
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: bin
|
||||
|
||||
- name: Download llap ui artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: ui
|
||||
path: web
|
||||
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v4
|
||||
- name: Setup buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
@@ -691,7 +691,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: bin/
|
||||
- name: Check file
|
||||
@@ -712,7 +712,7 @@ jobs:
|
||||
chmod +x bin/*-lldap_set_password
|
||||
|
||||
- name: Download llap ui artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: ui
|
||||
path: web
|
||||
|
||||
8
.github/workflows/rust.yml
vendored
8
.github/workflows/rust.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v5.0.0
|
||||
uses: actions/checkout@v6.0.2
|
||||
- name: Install Rust
|
||||
id: toolchain
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v5.0.0
|
||||
uses: actions/checkout@v6.0.2
|
||||
- name: Install Rust
|
||||
id: toolchain
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v5.0.0
|
||||
uses: actions/checkout@v6.0.2
|
||||
- name: Install Rust
|
||||
id: toolchain
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v5.0.0
|
||||
uses: actions/checkout@v6.0.2
|
||||
|
||||
- name: Install Rust
|
||||
run: rustup toolchain install nightly --component llvm-tools-preview && rustup component add llvm-tools-preview --toolchain stable-x86_64-unknown-linux-gnu
|
||||
|
||||
49
CHANGELOG.md
49
CHANGELOG.md
@@ -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/),
|
||||
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
|
||||
|
||||
Small release, focused on LDAP improvements and ongoing maintenance.
|
||||
|
||||
3130
Cargo.lock
generated
3130
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
114
Cargo.toml
114
Cargo.toml
@@ -24,12 +24,122 @@ lto = true
|
||||
[profile.release.package.lldap_app]
|
||||
opt-level = 's'
|
||||
|
||||
[patch.crates-io.lber]
|
||||
git = 'https://github.com/inejge/ldap3/'
|
||||
[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"]
|
||||
|
||||
@@ -83,7 +83,7 @@ MySQL/MariaDB or PostgreSQL.
|
||||
|
||||
## Installation
|
||||
|
||||
It's possible to install lldap from OCI images ([docker](docs/install.md#with-docker)/[podman](docs/install.md#with-podman)), from [Kubernetes](docs/install.md#with-kubernetes), or from [a regular distribution package manager](docs/install.md/#from-a-package-repository) (Archlinux, Debian, CentOS, Fedora, OpenSuse, Ubuntu, FreeBSD).
|
||||
It's possible to install lldap from OCI images ([docker](docs/install.md#with-docker)/[podman](docs/install.md#with-podman)), from [Kubernetes](docs/install.md#with-kubernetes), [TrueNAS](docs/install.md#truenas-scale), or from [a regular distribution package manager](docs/install.md/#from-a-package-repository) (Archlinux, Debian, CentOS, Fedora, OpenSuse, Ubuntu, FreeBSD).
|
||||
|
||||
Building [from source](docs/install.md#from-source) and [cross-compiling](docs/install.md#cross-compilation) to a different hardware architecture is also supported.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lldap_app"
|
||||
version = "0.6.2"
|
||||
version = "0.6.3"
|
||||
description = "Frontend for LLDAP"
|
||||
edition.workspace = true
|
||||
include = ["src/**/*", "queries/**/*", "Cargo.toml", "../schema.graphql"]
|
||||
@@ -11,27 +11,33 @@ repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
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"
|
||||
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 }
|
||||
url-escape = "0.1.1"
|
||||
validator = "0.14"
|
||||
validator_derive = "0.14"
|
||||
wasm-bindgen = "0.2.100"
|
||||
wasm-bindgen-futures = "*"
|
||||
wasm-bindgen-futures = "0"
|
||||
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 = [
|
||||
@@ -50,17 +56,6 @@ 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" ]
|
||||
@@ -71,18 +66,6 @@ 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"
|
||||
|
||||
@@ -112,7 +112,7 @@ impl CommonComponent<CreateGroupForm> for CreateGroupForm {
|
||||
let model = self.form.model();
|
||||
let req = create_group::Variables {
|
||||
group: create_group::CreateGroupInput {
|
||||
displayName: model.groupname,
|
||||
display_name: model.groupname,
|
||||
attributes,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -143,9 +143,9 @@ impl CommonComponent<CreateUserForm> for CreateUserForm {
|
||||
user: create_user::CreateUserInput {
|
||||
id: model.username,
|
||||
email: None,
|
||||
displayName: None,
|
||||
firstName: None,
|
||||
lastName: None,
|
||||
display_name: None,
|
||||
first_name: None,
|
||||
last_name: None,
|
||||
avatar: None,
|
||||
attributes,
|
||||
},
|
||||
@@ -304,11 +304,14 @@ impl Component for CreateUserForm {
|
||||
}
|
||||
|
||||
fn get_custom_attribute_input(attribute_schema: &Attribute) -> Html {
|
||||
let mail_is_required = attribute_schema.name.as_str() == "mail";
|
||||
|
||||
if attribute_schema.is_list {
|
||||
html! {
|
||||
<ListAttributeInput
|
||||
name={attribute_schema.name.clone()}
|
||||
attribute_type={attribute_schema.attribute_type}
|
||||
required={mail_is_required}
|
||||
/>
|
||||
}
|
||||
} else {
|
||||
@@ -316,6 +319,7 @@ fn get_custom_attribute_input(attribute_schema: &Attribute) -> Html {
|
||||
<SingleAttributeInput
|
||||
name={attribute_schema.name.clone()}
|
||||
attribute_type={attribute_schema.attribute_type}
|
||||
required={mail_is_required}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,8 @@ fn attribute_input(props: &AttributeInputProps) -> Html {
|
||||
#[derive(Properties, PartialEq)]
|
||||
struct AttributeLabelProps {
|
||||
pub name: String,
|
||||
#[prop_or(false)]
|
||||
pub required: bool,
|
||||
}
|
||||
#[function_component(AttributeLabel)]
|
||||
fn attribute_label(props: &AttributeLabelProps) -> Html {
|
||||
@@ -66,7 +68,9 @@ fn attribute_label(props: &AttributeLabelProps) -> Html {
|
||||
<label for={props.name.clone()}
|
||||
class="form-label col-4 col-form-label"
|
||||
>
|
||||
{props.name[0..1].to_uppercase() + &props.name[1..].replace('_', " ")}{":"}
|
||||
{props.name[0..1].to_uppercase() + &props.name[1..].replace('_', " ")}
|
||||
{if props.required { html!{<span class="text-danger">{"*"}</span>} } else { html!{} }}
|
||||
{":"}
|
||||
<button
|
||||
class="btn btn-sm btn-link"
|
||||
type="button"
|
||||
@@ -85,13 +89,15 @@ pub struct SingleAttributeInputProps {
|
||||
pub(crate) attribute_type: AttributeType,
|
||||
#[prop_or(None)]
|
||||
pub value: Option<String>,
|
||||
#[prop_or(false)]
|
||||
pub required: bool,
|
||||
}
|
||||
|
||||
#[function_component(SingleAttributeInput)]
|
||||
pub fn single_attribute_input(props: &SingleAttributeInputProps) -> Html {
|
||||
html! {
|
||||
<div class="row mb-3">
|
||||
<AttributeLabel name={props.name.clone()} />
|
||||
<AttributeLabel name={props.name.clone()} required={props.required} />
|
||||
<div class="col-8">
|
||||
<AttributeInput
|
||||
attribute_type={props.attribute_type}
|
||||
@@ -108,6 +114,8 @@ pub struct ListAttributeInputProps {
|
||||
pub(crate) attribute_type: AttributeType,
|
||||
#[prop_or(vec!())]
|
||||
pub values: Vec<String>,
|
||||
#[prop_or(false)]
|
||||
pub required: bool,
|
||||
}
|
||||
|
||||
pub enum ListAttributeInputMsg {
|
||||
@@ -160,7 +168,7 @@ impl Component for ListAttributeInput {
|
||||
let link = &ctx.link();
|
||||
html! {
|
||||
<div class="row mb-3">
|
||||
<AttributeLabel name={props.name.clone()} />
|
||||
<AttributeLabel name={props.name.clone()} required={props.required} />
|
||||
<div class="col-8">
|
||||
{self.indices.iter().map(|&i| html! {
|
||||
<div class="input-group mb-2" key={i}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::{fmt::Display, str::FromStr};
|
||||
|
||||
use anyhow::{Error, Ok, Result, bail};
|
||||
use base64::Engine;
|
||||
use gloo_file::{
|
||||
File,
|
||||
callbacks::{FileReader, read_as_bytes},
|
||||
@@ -54,12 +55,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::encode(data))
|
||||
Ok(base64::engine::general_purpose::STANDARD.encode(data))
|
||||
}
|
||||
JsFile {
|
||||
file: None,
|
||||
contents: Some(data),
|
||||
} => Ok(base64::encode(data)),
|
||||
} => Ok(base64::engine::general_purpose::STANDARD.encode(data)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +99,7 @@ impl Component for JpegFileInput {
|
||||
.props()
|
||||
.value
|
||||
.as_ref()
|
||||
.and_then(|x| base64::decode(x).ok()),
|
||||
.and_then(|x| base64::engine::general_purpose::STANDARD.decode(x).ok()),
|
||||
}),
|
||||
reader: None,
|
||||
}
|
||||
@@ -111,7 +112,7 @@ impl Component for JpegFileInput {
|
||||
.props()
|
||||
.value
|
||||
.as_ref()
|
||||
.and_then(|x| base64::decode(x).ok()),
|
||||
.and_then(|x| base64::engine::general_purpose::STANDARD.decode(x).ok()),
|
||||
});
|
||||
self.reader = None;
|
||||
true
|
||||
@@ -230,7 +231,7 @@ impl JpegFileInput {
|
||||
}
|
||||
|
||||
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()
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
@@ -248,13 +248,13 @@ impl GroupDetailsForm {
|
||||
};
|
||||
let mut group_input = update_group::UpdateGroupInput {
|
||||
id: self.group.id,
|
||||
displayName: None,
|
||||
removeAttributes: None,
|
||||
insertAttributes: None,
|
||||
display_name: None,
|
||||
remove_attributes: None,
|
||||
insert_attributes: None,
|
||||
};
|
||||
let default_group_input = group_input.clone();
|
||||
group_input.removeAttributes = remove_attributes;
|
||||
group_input.insertAttributes = insert_attributes;
|
||||
group_input.remove_attributes = remove_attributes;
|
||||
group_input.insert_attributes = insert_attributes;
|
||||
// Nothing changed.
|
||||
if group_input == default_group_input {
|
||||
return Ok(false);
|
||||
|
||||
@@ -27,7 +27,7 @@ pub struct LoginForm {
|
||||
pub struct FormModel {
|
||||
#[validate(length(min = 1, message = "Missing username"))]
|
||||
username: String,
|
||||
#[validate(length(min = 8, message = "Invalid password. Min length: 8"))]
|
||||
#[validate(length(min = 1, message = "Missing password"))]
|
||||
password: String,
|
||||
}
|
||||
|
||||
|
||||
@@ -260,16 +260,16 @@ impl UserDetailsForm {
|
||||
let mut user_input = update_user::UpdateUserInput {
|
||||
id: self.user.id.clone(),
|
||||
email: None,
|
||||
displayName: None,
|
||||
firstName: None,
|
||||
lastName: None,
|
||||
display_name: None,
|
||||
first_name: None,
|
||||
last_name: None,
|
||||
avatar: None,
|
||||
removeAttributes: None,
|
||||
insertAttributes: None,
|
||||
remove_attributes: None,
|
||||
insert_attributes: None,
|
||||
};
|
||||
let default_user_input = user_input.clone();
|
||||
user_input.removeAttributes = remove_attributes;
|
||||
user_input.insertAttributes = insert_attributes;
|
||||
user_input.remove_attributes = remove_attributes;
|
||||
user_input.insert_attributes = insert_attributes;
|
||||
// Nothing changed.
|
||||
if user_input == default_user_input {
|
||||
return Ok(false);
|
||||
|
||||
@@ -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: [{}]",
|
||||
|
||||
@@ -8,7 +8,7 @@ pub mod group {
|
||||
|
||||
use super::AttributeDescription;
|
||||
|
||||
pub fn resolve_group_attribute_description(name: &str) -> Option<AttributeDescription<'_>> {
|
||||
pub fn resolve_group_attribute_description(name: &'_ str) -> Option<AttributeDescription<'_>> {
|
||||
match name {
|
||||
"creation_date" => Some(AttributeDescription {
|
||||
attribute_identifier: name,
|
||||
@@ -39,7 +39,9 @@ pub mod group {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_group_attribute_description_or_default(name: &str) -> AttributeDescription<'_> {
|
||||
pub fn resolve_group_attribute_description_or_default(
|
||||
name: &'_ str,
|
||||
) -> AttributeDescription<'_> {
|
||||
match resolve_group_attribute_description(name) {
|
||||
Some(d) => d,
|
||||
None => AttributeDescription {
|
||||
@@ -55,7 +57,7 @@ pub mod user {
|
||||
|
||||
use super::AttributeDescription;
|
||||
|
||||
pub fn resolve_user_attribute_description(name: &str) -> Option<AttributeDescription<'_>> {
|
||||
pub fn resolve_user_attribute_description(name: &'_ str) -> Option<AttributeDescription<'_>> {
|
||||
match name {
|
||||
"avatar" => Some(AttributeDescription {
|
||||
attribute_identifier: name,
|
||||
@@ -111,7 +113,9 @@ pub mod user {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_user_attribute_description_or_default(name: &str) -> AttributeDescription<'_> {
|
||||
pub fn resolve_user_attribute_description_or_default(
|
||||
name: &'_ str,
|
||||
) -> AttributeDescription<'_> {
|
||||
match resolve_user_attribute_description(name) {
|
||||
Some(d) => d,
|
||||
None => AttributeDescription {
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub type DateTimeUtc = chrono::DateTime<chrono::Utc>;
|
||||
pub type DateTime = chrono::DateTime<chrono::Utc>;
|
||||
pub type DateTimeUtc = DateTime;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lldap_access_control"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
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 = "*"
|
||||
async-trait = "0.1"
|
||||
tracing = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
|
||||
[dependencies.lldap_auth]
|
||||
path = "../auth"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lldap_auth"
|
||||
version = "0.6.0"
|
||||
version = "0.6.3"
|
||||
description = "Authentication protocol for LLDAP"
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
@@ -18,35 +18,23 @@ 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 = "0.8"
|
||||
rand = { workspace = true }
|
||||
sha2 = "0.9"
|
||||
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"]
|
||||
thiserror = { workspace = true }
|
||||
uuid = { workspace = true, 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"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lldap_domain_handlers"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
homepage.workspace = true
|
||||
@@ -12,22 +12,17 @@ rust-version.workspace = true
|
||||
test = []
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1"
|
||||
base64 = "0.21"
|
||||
ldap3_proto = "0.6.0"
|
||||
serde_bytes = "0.11"
|
||||
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 }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1"
|
||||
|
||||
[dependencies.chrono]
|
||||
features = ["serde"]
|
||||
version = "0.4"
|
||||
|
||||
[dependencies.derive_more]
|
||||
features = ["debug", "display", "from", "from_str"]
|
||||
default-features = false
|
||||
version = "1"
|
||||
pretty_assertions = { workspace = true }
|
||||
|
||||
[dependencies.lldap_auth]
|
||||
path = "../auth"
|
||||
@@ -38,10 +33,3 @@ path = "../domain"
|
||||
|
||||
[dependencies.lldap_domain_model]
|
||||
path = "../domain-model"
|
||||
|
||||
[dependencies.serde]
|
||||
workspace = true
|
||||
|
||||
[dependencies.uuid]
|
||||
features = ["v1", "v3"]
|
||||
version = "1"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lldap_domain_model"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
homepage.workspace = true
|
||||
@@ -12,23 +12,19 @@ rust-version.workspace = true
|
||||
test = []
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.21"
|
||||
bincode = "1.3"
|
||||
orion = "0.17"
|
||||
serde_bytes = "0.11"
|
||||
thiserror = "2"
|
||||
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 }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1"
|
||||
|
||||
[dependencies.chrono]
|
||||
features = ["serde"]
|
||||
version = "0.4"
|
||||
|
||||
[dependencies.derive_more]
|
||||
features = ["debug", "display", "from", "from_str"]
|
||||
default-features = false
|
||||
version = "1"
|
||||
pretty_assertions = { workspace = true }
|
||||
|
||||
[dependencies.lldap_auth]
|
||||
path = "../auth"
|
||||
@@ -36,14 +32,3 @@ 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"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lldap_domain"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
authors = [
|
||||
"Valentin Tolmer <valentin@tolmer.fr>",
|
||||
"Simon Broeng Jensen <sbj@cwconsult.dk>",
|
||||
@@ -15,51 +15,23 @@ rust-version.workspace = true
|
||||
test = []
|
||||
|
||||
[dependencies]
|
||||
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"
|
||||
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 }
|
||||
|
||||
[dependencies.lldap_auth]
|
||||
path = "../auth"
|
||||
features = ["opaque_server", "opaque_client", "sea_orm"]
|
||||
|
||||
[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"
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
|
||||
@@ -7,8 +7,8 @@ use sea_orm::{
|
||||
DbErr, DeriveValueType, QueryResult, TryFromU64, TryGetError, TryGetable, Value,
|
||||
entity::IntoActiveValue,
|
||||
sea_query::{
|
||||
ArrayType, ColumnType, Nullable, SeaRc, StringLen, ValueTypeErr,
|
||||
extension::mysql::MySqlType, value::ValueType,
|
||||
ArrayType, ColumnType, SeaRc, StringLen, ValueTypeErr, extension::mysql::MySqlType,
|
||||
value::ValueType,
|
||||
},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -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::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()?;
|
||||
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::io::Reader::with_format(
|
||||
image::ImageReader::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::{ImageOutputFormat, Rgb, RgbImage};
|
||||
use image::{ImageFormat, Rgb, RgbImage};
|
||||
let img = RgbImage::from_fn(32, 32, |x, y| {
|
||||
if (x + y) % 2 == 0 {
|
||||
Rgb([0, 0, 0])
|
||||
@@ -406,21 +406,12 @@ impl JpegPhoto {
|
||||
}
|
||||
});
|
||||
let mut bytes: Vec<u8> = Vec::new();
|
||||
img.write_to(
|
||||
&mut std::io::Cursor::new(&mut bytes),
|
||||
ImageOutputFormat::Jpeg(0),
|
||||
)
|
||||
.unwrap();
|
||||
img.write_to(&mut std::io::Cursor::new(&mut bytes), ImageFormat::Jpeg)
|
||||
.unwrap();
|
||||
Self(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl Nullable for JpegPhoto {
|
||||
fn null() -> Value {
|
||||
JpegPhoto::null().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoActiveValue<Serialized> for JpegPhoto {
|
||||
fn into_active_value(self) -> sea_orm::ActiveValue<Serialized> {
|
||||
if self.is_empty() {
|
||||
@@ -694,7 +685,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
&format!("{:?}", Serialized::from(&JpegPhoto::for_tests())),
|
||||
"Serialized(\"hash: 0xB947C77A16F3C3BD\")"
|
||||
"Serialized(\"hash: 0xBB3017828B2F3DEF\")"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lldap_frontend_options"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
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 }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lldap_graphql_server"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
description = "GraphQL server for LLDAP"
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
@@ -10,15 +10,14 @@ repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = "*"
|
||||
juniper = "0.15"
|
||||
serde_json = "1"
|
||||
tracing = "*"
|
||||
urlencoding = "2"
|
||||
|
||||
[dependencies.chrono]
|
||||
features = ["serde"]
|
||||
version = "*"
|
||||
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 }
|
||||
|
||||
[dependencies.lldap_access_control]
|
||||
path = "../access-control"
|
||||
@@ -45,16 +44,10 @@ path = "../sql-backend-handler"
|
||||
[dependencies.lldap_validation]
|
||||
path = "../validation"
|
||||
|
||||
[dependencies.serde]
|
||||
workspace = true
|
||||
|
||||
[dependencies.uuid]
|
||||
features = ["v1", "v3"]
|
||||
version = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
mockall = "0.11.4"
|
||||
pretty_assertions = "1"
|
||||
mockall = { workspace = true }
|
||||
pretty_assertions = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
|
||||
#[dev-dependencies.lldap_auth]
|
||||
#path = "../auth"
|
||||
@@ -70,7 +63,3 @@ path = "../test-utils"
|
||||
#[dev-dependencies.lldap_sql_backend_handler]
|
||||
#path = "../sql-backend-handler"
|
||||
#features = ["test"]
|
||||
|
||||
[dev-dependencies.tokio]
|
||||
features = ["full"]
|
||||
version = "1.25"
|
||||
|
||||
@@ -60,7 +60,7 @@ impl<Handler: BackendHandler> Context<Handler> {
|
||||
impl<Handler: BackendHandler> juniper::Context for Context<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> {
|
||||
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_schema_language();
|
||||
let output = schema::<SqlBackendHandler>().as_sdl();
|
||||
match output_file {
|
||||
None => println!("{output}"),
|
||||
Some(path) => {
|
||||
|
||||
@@ -561,13 +561,13 @@ mod tests {
|
||||
use mockall::predicate::eq;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn mutation_schema<'q, C, Q, M>(
|
||||
fn mutation_schema<C, Q, M>(
|
||||
query_root: Q,
|
||||
mutation_root: M,
|
||||
) -> RootNode<'q, Q, M, EmptySubscription<C>>
|
||||
) -> RootNode<Q, M, EmptySubscription<C>>
|
||||
where
|
||||
Q: GraphQLType<DefaultScalarValue, Context = C, TypeInfo = ()> + 'q,
|
||||
M: GraphQLType<DefaultScalarValue, Context = C, TypeInfo = ()> + 'q,
|
||||
Q: GraphQLType<DefaultScalarValue, Context = C, TypeInfo = ()>,
|
||||
M: GraphQLType<DefaultScalarValue, Context = C, TypeInfo = ()>,
|
||||
{
|
||||
RootNode::new(query_root, mutation_root, EmptySubscription::<C>::new())
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,11 @@ impl<Handler: BackendHandler> Query<Handler> {
|
||||
"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;
|
||||
let span = debug_span!("[GraphQL query] user");
|
||||
span.in_scope(|| {
|
||||
@@ -61,14 +65,15 @@ impl<Handler: BackendHandler> Query<Handler> {
|
||||
&span,
|
||||
"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?;
|
||||
User::<Handler>::from_user(user, schema)
|
||||
}
|
||||
|
||||
async fn users(
|
||||
&self,
|
||||
context: &Context<Handler>,
|
||||
#[graphql(name = "where")] filters: Option<RequestFilter>,
|
||||
filters: Option<RequestFilter>,
|
||||
) -> FieldResult<Vec<User<Handler>>> {
|
||||
let span = debug_span!("[GraphQL query] users");
|
||||
span.in_scope(|| {
|
||||
@@ -80,7 +85,7 @@ impl<Handler: BackendHandler> Query<Handler> {
|
||||
&span,
|
||||
"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
|
||||
.list_users(
|
||||
filters
|
||||
@@ -96,7 +101,7 @@ impl<Handler: BackendHandler> Query<Handler> {
|
||||
.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 handler = context
|
||||
.get_readonly_handler()
|
||||
@@ -104,7 +109,7 @@ impl<Handler: BackendHandler> Query<Handler> {
|
||||
&span,
|
||||
"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?;
|
||||
domain_groups
|
||||
.into_iter()
|
||||
@@ -112,7 +117,11 @@ impl<Handler: BackendHandler> Query<Handler> {
|
||||
.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");
|
||||
span.in_scope(|| {
|
||||
debug!(?group_id);
|
||||
@@ -123,7 +132,7 @@ impl<Handler: BackendHandler> Query<Handler> {
|
||||
&span,
|
||||
"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
|
||||
.get_group_details(GroupId(group_id))
|
||||
.instrument(span)
|
||||
@@ -131,7 +140,7 @@ impl<Handler: BackendHandler> Query<Handler> {
|
||||
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");
|
||||
self.get_schema(context, span).await.map(Into::into)
|
||||
}
|
||||
@@ -175,9 +184,9 @@ mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
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
|
||||
Q: GraphQLType<DefaultScalarValue, Context = C, TypeInfo = ()> + 'q,
|
||||
Q: GraphQLType<DefaultScalarValue, Context = C, TypeInfo = ()>,
|
||||
{
|
||||
RootNode::new(
|
||||
query_root,
|
||||
|
||||
@@ -70,7 +70,7 @@ impl<Handler: BackendHandler> User<Handler> {
|
||||
fn first_name(&self) -> &str {
|
||||
self.attributes
|
||||
.iter()
|
||||
.find(|a| a.name() == "first_name")
|
||||
.find(|a| a.attribute_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.name() == "last_name")
|
||||
.find(|a| a.attribute_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.name() == "avatar")
|
||||
.find(|a| a.attribute_name() == "avatar")
|
||||
.map(|a| {
|
||||
String::from(
|
||||
a.attribute
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lldap_ldap"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
description = "LDAP operations support"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
@@ -10,27 +10,19 @@ repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = "*"
|
||||
ldap3_proto = "0.6.0"
|
||||
tracing = "*"
|
||||
itertools = "0.10"
|
||||
anyhow = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
|
||||
[dependencies.derive_more]
|
||||
features = ["from"]
|
||||
default-features = false
|
||||
version = "1"
|
||||
derive_more = { workspace = true }
|
||||
|
||||
[dependencies.chrono]
|
||||
features = ["serde"]
|
||||
version = "*"
|
||||
chrono = { workspace = true }
|
||||
|
||||
[dependencies.rand]
|
||||
features = ["small_rng", "getrandom"]
|
||||
version = "0.8"
|
||||
rand = { workspace = true }
|
||||
|
||||
[dependencies.uuid]
|
||||
version = "1"
|
||||
features = ["v1", "v3"]
|
||||
uuid = { workspace = true }
|
||||
|
||||
ldap3_proto = { workspace = true }
|
||||
|
||||
[dependencies.lldap_access_control]
|
||||
path = "../access-control"
|
||||
@@ -55,12 +47,10 @@ path = "../opaque-handler"
|
||||
path = "../test-utils"
|
||||
|
||||
[dev-dependencies]
|
||||
mockall = "0.11.4"
|
||||
pretty_assertions = "1"
|
||||
mockall = { workspace = true }
|
||||
pretty_assertions = { workspace = true }
|
||||
|
||||
[dev-dependencies.tokio]
|
||||
features = ["full"]
|
||||
version = "1.25"
|
||||
tokio = { workspace = true }
|
||||
|
||||
[dev-dependencies.lldap_domain]
|
||||
path = "../domain"
|
||||
|
||||
@@ -261,6 +261,21 @@ fn convert_user_filter(
|
||||
UserColumn::LowercaseEmail,
|
||||
value_lc,
|
||||
)),
|
||||
UserFieldType::PrimaryField(UserColumn::DisplayName) => {
|
||||
// DisplayName (cn) should match case-insensitively, so we try both
|
||||
// the original value and the lowercase value (if different)
|
||||
if value.as_str() == value_lc {
|
||||
Ok(UserRequestFilter::Equality(
|
||||
UserColumn::DisplayName,
|
||||
value_lc,
|
||||
))
|
||||
} else {
|
||||
Ok(UserRequestFilter::Or(vec![
|
||||
UserRequestFilter::Equality(UserColumn::DisplayName, value.to_string()),
|
||||
UserRequestFilter::Equality(UserColumn::DisplayName, value_lc),
|
||||
]))
|
||||
}
|
||||
}
|
||||
UserFieldType::PrimaryField(field) => {
|
||||
Ok(UserRequestFilter::Equality(field, value_lc))
|
||||
}
|
||||
@@ -770,4 +785,47 @@ mod tests {
|
||||
panic!("Expected SearchResultEntry");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_search_cn_case_insensitive() {
|
||||
use lldap_domain::uuid;
|
||||
let mut mock = MockTestBackendHandler::new();
|
||||
mock.expect_list_users()
|
||||
.with(
|
||||
eq(Some(UserRequestFilter::Or(vec![
|
||||
UserRequestFilter::Equality(UserColumn::DisplayName, "TestAll".to_string()),
|
||||
UserRequestFilter::Equality(UserColumn::DisplayName, "testall".to_string()),
|
||||
]))),
|
||||
eq(false),
|
||||
)
|
||||
.times(1)
|
||||
.return_once(|_, _| {
|
||||
Ok(vec![UserAndGroups {
|
||||
user: User {
|
||||
user_id: UserId::new("testall"),
|
||||
email: "test@example.com".into(),
|
||||
display_name: Some("TestAll".to_string()),
|
||||
uuid: uuid!("698e1d5f-7a40-3151-8745-b9b8a37839da"),
|
||||
attributes: vec![],
|
||||
..Default::default()
|
||||
},
|
||||
groups: None,
|
||||
}])
|
||||
});
|
||||
let ldap_handler = setup_bound_admin_handler(mock).await;
|
||||
let request = make_user_search_request(
|
||||
LdapFilter::Equality("cn".to_string(), "TestAll".to_string()),
|
||||
vec!["cn", "uid"],
|
||||
);
|
||||
let results = ldap_handler.do_search_or_dse(&request).await.unwrap();
|
||||
assert_eq!(results.len(), 2);
|
||||
if let LdapOp::SearchResultEntry(entry) = &results[0] {
|
||||
assert_eq!(entry.dn, "uid=testall,ou=people,dc=example,dc=com");
|
||||
assert_eq!(entry.attributes.len(), 2);
|
||||
assert_eq!(entry.attributes[0].atype, "cn");
|
||||
assert_eq!(entry.attributes[0].vals[0], b"TestAll");
|
||||
} else {
|
||||
panic!("Expected SearchResultEntry");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,6 +421,14 @@ pub async fn do_search(
|
||||
InternalSearchResults::Raw(raw_results) => raw_results,
|
||||
InternalSearchResults::Empty => Vec::new(),
|
||||
};
|
||||
// RFC 4511: When performing a base scope search, if the entry doesn't exist,
|
||||
// we should return NoSuchObject instead of Success with zero entries
|
||||
if results.is_empty() && request.scope == LdapSearchScope::Base {
|
||||
return Err(LdapError {
|
||||
code: LdapResultCode::NoSuchObject,
|
||||
message: "".to_string(),
|
||||
});
|
||||
}
|
||||
if !matches!(results.last(), Some(LdapOp::SearchResultDone(_))) {
|
||||
results.push(make_search_success());
|
||||
}
|
||||
@@ -1447,4 +1455,76 @@ mod tests {
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_search_base_scope_non_existent_user() {
|
||||
let mut mock = MockTestBackendHandler::new();
|
||||
mock.expect_list_users().returning(|_, _| Ok(vec![]));
|
||||
let ldap_handler = setup_bound_admin_handler(mock).await;
|
||||
let request = LdapSearchRequest {
|
||||
scope: LdapSearchScope::Base,
|
||||
..make_search_request(
|
||||
"uid=nonexistent,ou=people,dc=example,dc=com",
|
||||
LdapFilter::And(vec![]),
|
||||
vec!["objectClass".to_string()],
|
||||
)
|
||||
};
|
||||
assert_eq!(
|
||||
ldap_handler.do_search_or_dse(&request).await,
|
||||
Err(LdapError {
|
||||
code: LdapResultCode::NoSuchObject,
|
||||
message: "".to_string(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_search_base_scope_non_existent_group() {
|
||||
let mut mock = MockTestBackendHandler::new();
|
||||
mock.expect_list_groups().returning(|_| Ok(vec![]));
|
||||
let ldap_handler = setup_bound_admin_handler(mock).await;
|
||||
let request = LdapSearchRequest {
|
||||
scope: LdapSearchScope::Base,
|
||||
..make_search_request(
|
||||
"uid=nonexistent,ou=groups,dc=example,dc=com",
|
||||
LdapFilter::And(vec![]),
|
||||
vec!["objectClass".to_string()],
|
||||
)
|
||||
};
|
||||
assert_eq!(
|
||||
ldap_handler.do_search_or_dse(&request).await,
|
||||
Err(LdapError {
|
||||
code: LdapResultCode::NoSuchObject,
|
||||
message: "".to_string(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_search_base_scope_existing_user() {
|
||||
let mut mock = MockTestBackendHandler::new();
|
||||
mock.expect_list_users().returning(|_, _| {
|
||||
Ok(vec![UserAndGroups {
|
||||
user: User {
|
||||
user_id: UserId::new("bob"),
|
||||
..Default::default()
|
||||
},
|
||||
groups: None,
|
||||
}])
|
||||
});
|
||||
let ldap_handler = setup_bound_admin_handler(mock).await;
|
||||
let request = LdapSearchRequest {
|
||||
scope: LdapSearchScope::Base,
|
||||
..make_search_request(
|
||||
"uid=bob,ou=people,dc=example,dc=com",
|
||||
LdapFilter::And(vec![]),
|
||||
vec!["objectClass".to_string()],
|
||||
)
|
||||
};
|
||||
let results = ldap_handler.do_search_or_dse(&request).await.unwrap();
|
||||
// Should have 2 results: SearchResultEntry and SearchResultDone
|
||||
assert_eq!(results.len(), 2);
|
||||
assert!(matches!(results[0], LdapOp::SearchResultEntry(_)));
|
||||
assert!(matches!(results[1], LdapOp::SearchResultDone(_)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lldap_opaque_handler"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
description = "Opaque handler trait for LLDAP"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
@@ -13,7 +13,7 @@ rust-version.workspace = true
|
||||
test = []
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1"
|
||||
async-trait = { workspace = true }
|
||||
|
||||
[dependencies.lldap_auth]
|
||||
path = "../auth"
|
||||
@@ -26,4 +26,4 @@ path = "../domain"
|
||||
path = "../domain-model"
|
||||
|
||||
[dev-dependencies]
|
||||
mockall = "0.11.4"
|
||||
mockall = { workspace = true }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lldap_sql_backend_handler"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
description = "SQL backend for LLDAP"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
@@ -13,44 +13,30 @@ rust-version.workspace = true
|
||||
test = []
|
||||
|
||||
[dependencies]
|
||||
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 = "*"
|
||||
anyhow = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
orion = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[dependencies.chrono]
|
||||
features = ["serde"]
|
||||
version = "*"
|
||||
bincode = { workspace = true }
|
||||
|
||||
[dependencies.rand]
|
||||
features = ["small_rng", "getrandom"]
|
||||
version = "0.8"
|
||||
base64 = { workspace = true }
|
||||
|
||||
[dependencies.sea-orm]
|
||||
workspace = true
|
||||
features = [
|
||||
"macros",
|
||||
"with-chrono",
|
||||
"with-uuid",
|
||||
"sqlx-all",
|
||||
"runtime-actix-rustls",
|
||||
]
|
||||
chrono = { workspace = true }
|
||||
|
||||
[dependencies.secstr]
|
||||
features = ["serde"]
|
||||
version = "*"
|
||||
rand = { workspace = true }
|
||||
|
||||
[dependencies.serde]
|
||||
workspace = true
|
||||
sea-orm = { workspace = true }
|
||||
|
||||
[dependencies.uuid]
|
||||
version = "1"
|
||||
features = ["v1", "v3"]
|
||||
secstr = { workspace = true }
|
||||
|
||||
serde = { workspace = true }
|
||||
|
||||
uuid = { workspace = true }
|
||||
|
||||
ldap3_proto = { workspace = true }
|
||||
|
||||
[dependencies.lldap_access_control]
|
||||
path = "../access-control"
|
||||
@@ -71,18 +57,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 = "*"
|
||||
mockall = "0.11.4"
|
||||
pretty_assertions = "1"
|
||||
log = { workspace = true }
|
||||
mockall = { workspace = true }
|
||||
pretty_assertions = { workspace = true }
|
||||
|
||||
[dev-dependencies.tokio]
|
||||
features = ["full"]
|
||||
version = "1.25"
|
||||
tokio = { workspace = true }
|
||||
|
||||
[dev-dependencies.tracing-subscriber]
|
||||
version = "0.3"
|
||||
features = ["env-filter", "tracing-log"]
|
||||
tracing-subscriber = { workspace = true }
|
||||
|
||||
@@ -536,7 +536,7 @@ See https://github.com/lldap/lldap/blob/main/docs/migration_guides/v0.5.md for d
|
||||
Conflicting emails:
|
||||
"#,
|
||||
);
|
||||
for (email, users) in &transaction
|
||||
let duplicate_users = transaction
|
||||
.query_all(
|
||||
builder.build(
|
||||
Query::select()
|
||||
@@ -568,9 +568,8 @@ Conflicting emails:
|
||||
row.try_get::<String>("", &Users::Email.to_string())
|
||||
.unwrap(),
|
||||
)
|
||||
})
|
||||
.group_by(|(_user, email)| email.to_owned())
|
||||
{
|
||||
});
|
||||
for (email, users) in &duplicate_users.chunk_by(|(_user, email)| email.to_owned()) {
|
||||
warn!("Email: {email}");
|
||||
for (user, _email) in users {
|
||||
warn!(" User: {}", user.as_str());
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lldap_test_utils"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
homepage.workspace = true
|
||||
@@ -9,14 +9,13 @@ repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1"
|
||||
ldap3_proto = "0.6.0"
|
||||
mockall = "0.11.4"
|
||||
tracing = "*"
|
||||
async-trait = { workspace = true }
|
||||
mockall = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[dependencies.uuid]
|
||||
version = "1"
|
||||
features = ["v1", "v3"]
|
||||
uuid = { workspace = true }
|
||||
|
||||
ldap3_proto = { workspace = true }
|
||||
|
||||
[dependencies.lldap_access_control]
|
||||
path = "../access-control"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lldap_validation"
|
||||
version = "0.6.0"
|
||||
version = "0.6.3"
|
||||
authors = ["Simon Broeng Jensen <sbj@cwconsult.dk>"]
|
||||
description = "Validation logic for LLDAP"
|
||||
edition.workspace = true
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
- [With Docker](#with-docker)
|
||||
- [With Podman](#with-podman)
|
||||
- [With Kubernetes](#with-kubernetes)
|
||||
- [TrueNAS SCALE](#truenas-scale)
|
||||
- [From a package repository](#from-a-package-repository)
|
||||
- [With FreeBSD](#with-freebsd)
|
||||
- [From source](#from-source)
|
||||
@@ -105,6 +106,27 @@ You can bootstrap your lldap instance (users, groups)
|
||||
using [bootstrap.sh](../example_configs/bootstrap/bootstrap.md#kubernetes-job).
|
||||
It can be run by Argo CD for managing users in git-opt way, or as a one-shot job.
|
||||
|
||||
### TrueNAS SCALE
|
||||
|
||||
LLDAP can be installed on **TrueNAS SCALE** using the built-in Apps catalog, allowing users to deploy and manage LLDAP directly from the TrueNAS web interface without manually maintaining containers.
|
||||
|
||||
To install:
|
||||
|
||||
1. Open the TrueNAS web interface.
|
||||
2. Navigate to **Apps → Discover Apps**.
|
||||
3. Search for **LLDAP** and click **Install**.
|
||||
4. Provide the required configuration values such as:
|
||||
- Base DN
|
||||
- Admin credentials
|
||||
- LDAP / LDAPS ports
|
||||
- Persistent storage dataset
|
||||
|
||||
TrueNAS supports selecting certificates for LDAPS and configuring a public web URL. When LDAPS is enabled, it is recommended to disable the unencrypted LDAP port to ensure secure communication.
|
||||
|
||||
A full, step-by-step TrueNAS-specific guide (including recommended ports, certificate configuration, and common integrations) is available here:
|
||||
|
||||
👉 [example_configs/truenas-install.md](https://github.com/lldap/lldap/blob/main/example_configs/truenas-install.md)
|
||||
|
||||
### From a package repository
|
||||
|
||||
**Do not open issues in this repository for problems with third-party
|
||||
|
||||
@@ -4,6 +4,7 @@ Some specific clients have been tested to work and come with sample
|
||||
configuration files:
|
||||
|
||||
- [Airsonic Advanced](airsonic-advanced.md)
|
||||
- [Apache HTTP Server](apache.md)
|
||||
- [Apache Guacamole](apacheguacamole.md)
|
||||
- [Apereo CAS Server](apereo_cas_server.md)
|
||||
- [Authelia](authelia.md)
|
||||
@@ -11,6 +12,7 @@ configuration files:
|
||||
- [Bookstack](bookstack.env.example)
|
||||
- [Calibre-Web](calibre_web.md)
|
||||
- [Carpal](carpal.md)
|
||||
- [Continuwuity](continuwuity.md)
|
||||
- [Dell iDRAC](dell_idrac.md)
|
||||
- [Dex](dex_config.yml)
|
||||
- [Dokuwiki](dokuwiki.md)
|
||||
@@ -19,6 +21,7 @@ configuration files:
|
||||
- [Ejabberd](ejabberd.md)
|
||||
- [Emby](emby.md)
|
||||
- [Ergo IRCd](ergo.md)
|
||||
- [Gerrit](gerrit.md)
|
||||
- [Gitea](gitea.md)
|
||||
- [GitLab](gitlab.md)
|
||||
- [Grafana](grafana_ldap_config.toml)
|
||||
@@ -47,6 +50,7 @@ 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)
|
||||
@@ -60,6 +64,7 @@ configuration files:
|
||||
- [Radicale](radicale.md)
|
||||
- [Rancher](rancher.md)
|
||||
- [Seafile](seafile.md)
|
||||
- [Semaphore](semaphore.md)
|
||||
- [Shaarli](shaarli.md)
|
||||
- [Snipe-IT](snipe-it.md)
|
||||
- [SonarQube](sonarqube.md)
|
||||
|
||||
65
example_configs/apache.md
Normal file
65
example_configs/apache.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# Configuration for Apache
|
||||
|
||||
This example snippet provides space under `/webdav/<username>/` if they log in as the user in question.
|
||||
|
||||
## Apache LDAP Configuration
|
||||
|
||||
```
|
||||
# The User/Group specified in httpd.conf needs to have write permissions
|
||||
# on the directory where the DavLockDB is placed and on any directory where
|
||||
# "Dav On" is specified.
|
||||
|
||||
DavLockDB "/var/local/apache2/DavLock"
|
||||
|
||||
Alias /webdav "/var/local/apache2/data"
|
||||
|
||||
<Directory "/var/local/apache2/data">
|
||||
AllowOverride None
|
||||
Require all denied
|
||||
DirectoryIndex disabled
|
||||
</Directory>
|
||||
|
||||
<DirectoryMatch "^/var/local/apache2/data/(?<user>[^/]+)">
|
||||
AuthType Basic
|
||||
AuthName "LDAP Credentials"
|
||||
AuthBasicProvider ldap
|
||||
|
||||
AuthLDAPURL ldap://lldap:3890/ou=people,dc=example,dc=com?uid?sub?(objectClass=person)
|
||||
AuthLDAPBindDN uid=integration,ou=people,dc=example,dc=com
|
||||
AuthLDAPBindPassword [redacted]
|
||||
|
||||
<RequireAll>
|
||||
Require ldap-user "%{env:MATCH_USER}"
|
||||
Require ldap-group cn=WebDAV,ou=groups,dc=example,dc=com
|
||||
</RequireAll>
|
||||
|
||||
Dav On
|
||||
Options +Indexes
|
||||
</DirectoryMatch>
|
||||
```
|
||||
### Notes
|
||||
|
||||
* Make sure you create the `data` directory, and the subdirectories for your users.
|
||||
* `integration` was an LDAP user I added with strict readonly.
|
||||
* The `WebDAV` group was something I added and put relevant users into, more as a test of functionality than out of any need.
|
||||
* I left the comment from the Apache DAV config in because it's not kidding around and it won't be obvious what's going wrong from the Apache logs if you miss that.
|
||||
|
||||
## Apache Orchestration
|
||||
|
||||
The stock Apache server with that stanza added to the bottom of the stock config and shared into the container.
|
||||
```
|
||||
webdav:
|
||||
image: httpd:2.4.66-trixie
|
||||
restart: always
|
||||
volumes:
|
||||
- /opt/webdav:/var/local/apache2
|
||||
- ./httpd.conf:/usr/local/apache2/conf/httpd.conf
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.webdav.entrypoints=websecure"
|
||||
- "traefik.http.routers.webdav.rule=Host(`redacted`) && PathPrefix(`/webdav`)"
|
||||
- "traefik.http.routers.webdav.tls.certresolver=myresolver"
|
||||
- "traefik.http.routers.webdav.service=webdav-service"
|
||||
- "traefik.http.services.webdav-service.loadbalancer.server.port=80"
|
||||
```
|
||||
|
||||
@@ -72,6 +72,7 @@ Fields description:
|
||||
* `id`: it's just username (**MANDATORY**)
|
||||
* `email`: self-explanatory (**MANDATORY**)
|
||||
* `password`: would be used to set the password using `lldap_set_password` utility
|
||||
* `password_file`: path to a file containing the password otherwise same as above
|
||||
* `displayName`: self-explanatory
|
||||
* `firstName`: self-explanatory
|
||||
* `lastName`: self-explanatory
|
||||
|
||||
15
example_configs/continuwuity.md
Normal file
15
example_configs/continuwuity.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Configuration for Continuwuity
|
||||
|
||||
This example is with environment vars from my docker-compose.yml, this also works just as well with a [config file](https://continuwuity.org/reference/config). `uid=query,ou=people,dc=example,dc=com` is a read-only user and you need to put their password into `/etc/bind_password_file`. Users need to be in the group `matrix` to log in and users in the group `matrix-admin` will be an admin.
|
||||
|
||||
```
|
||||
CONTINUWUITY_LDAP__ENABLE: 'true'
|
||||
CONTINUWUITY_LDAP__LDAP_ONLY: 'true'
|
||||
CONTINUWUITY_LDAP__URI: 'ldap://lldap.example.com:3890'
|
||||
CONTINUWUITY_LDAP__BASE_DN: 'ou=people,dc=example,dc=com'
|
||||
CONTINUWUITY_LDAP__BIND_DN: 'uid=query,ou=people,dc=example,dc=com'
|
||||
CONTINUWUITY_LDAP__BIND_PASSWORD_FILE: '/etc/bind_password_file'
|
||||
CONTINUWUITY_LDAP__FILTER: '(memberOf=matrix)'
|
||||
CONTINUWUITY_LDAP__UID_ATTRIBUTE: 'uid'
|
||||
CONTINUWUITY_LDAP__ADMIN_FILTER: '(memberOf=matrix-admin)'
|
||||
```
|
||||
18
example_configs/gerrit.md
Normal file
18
example_configs/gerrit.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Configuration for Gerrit
|
||||
|
||||
Edit `gerrit.config`:
|
||||
```ini
|
||||
[auth]
|
||||
type = ldap
|
||||
|
||||
[ldap]
|
||||
server = ldap://lldap:3890
|
||||
supportAnonymous = false
|
||||
username = uid=gerritadmin,ou=people,dc=example.com,dc=com
|
||||
accountBase = ou=people,dc=example.com,dc=com
|
||||
accountPattern = (uid=${username})
|
||||
accountFullName = cn
|
||||
accountEmailAddress = mail
|
||||
```
|
||||
|
||||
The `supportAnonymous = false` must be set.
|
||||
@@ -41,7 +41,14 @@ name = "displayName"
|
||||
surname = "sn"
|
||||
username = "uid"
|
||||
|
||||
# If you want to map your ldap groups to grafana's groups, see: https://grafana.com/docs/grafana/latest/auth/ldap/#group-mappings
|
||||
# If you want to map your ldap groups to grafana's groups, configure the group query:
|
||||
# https://grafana.com/docs/grafana/latest/setup-grafana/configure-access/configure-authentication/ldap/#posix-schema
|
||||
# group_search_filter = "(&(objectClass=groupOfUniqueNames)(uniqueMember=%s))"
|
||||
# group_search_base_dns = ["ou=groups,dc=example,dc=com"]
|
||||
# group_search_filter_user_attribute = "uid"
|
||||
#
|
||||
# Then configure the groups:
|
||||
# https://grafana.com/docs/grafana/latest/setup-grafana/configure-access/configure-authentication/ldap/#group-mappings
|
||||
# As a quick example, here is how you would map lldap's admin group to grafana's admin
|
||||
# [[servers.group_mappings]]
|
||||
# group_dn = "cn=lldap_admin,ou=groups,dc=example,dc=org"
|
||||
|
||||
@@ -64,7 +64,7 @@ if [[ ! -z "$2" ]] && ! jq -e '.groups|map(.displayName)|index("'"$2"'")' <<< $U
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DISPLAY_NAME=$(jq -r .displayName <<< $USER_JSON)
|
||||
DISPLAY_NAME=$(jq -r '.displayName // .id' <<< $USER_JSON)
|
||||
|
||||
IS_ADMIN=false
|
||||
if [[ ! -z "$3" ]] && jq -e '.groups|map(.displayName)|index("'"$3"'")' <<< "$USER_JSON" > /dev/null 2>&1; then
|
||||
@@ -88,4 +88,4 @@ if [[ "$IS_LOCAL" = true ]]; then
|
||||
echo "local_only = true"
|
||||
else
|
||||
echo "local_only = false"
|
||||
fi
|
||||
fi
|
||||
|
||||
55
example_configs/opencloud.md
Normal file
55
example_configs/opencloud.md
Normal 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).
|
||||
37
example_configs/semaphore.md
Normal file
37
example_configs/semaphore.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Configuration for Semaphore
|
||||
|
||||
Semaphore configuration is in `config.json`
|
||||
|
||||
Just add the following lines:
|
||||
```json
|
||||
"ldap_enable": true,
|
||||
"ldap_needtls": true,
|
||||
"ldap_server": "ldaps_server:6360",
|
||||
"ldap_binddn": "uid=semaphorebind,ou=people,dc=example,dc=com",
|
||||
"ldap_bindpassword": "verysecretpassword",
|
||||
"ldap_searchdn": "ou=people,dc=example,dc=com",
|
||||
"ldap_searchfilter": "(|(uid=%[1]s)(mail=%[1]s))",
|
||||
"ldap_mappings": {
|
||||
"dn": "dn",
|
||||
"mail": "mail",
|
||||
"uid": "uid",
|
||||
"cn": "cn"
|
||||
}
|
||||
```
|
||||
|
||||
If you use environment variables:
|
||||
```bash
|
||||
Environment=SEMAPHORE_LDAP_ENABLE=true
|
||||
Environment=SEMAPHORE_LDAP_SERVER="ldaps_server:6360"
|
||||
Environment=SEMAPHORE_LDAP_NEEDTLS=true
|
||||
Environment=SEMAPHORE_LDAP_BIND_DN="uid=semaphorebind,ou=people,dc=example,dc=com"
|
||||
Environment=SEMAPHORE_LDAP_BIND_PASSWORD="verysecretpassword"
|
||||
Environment=SEMAPHORE_LDAP_SEARCH_DN="ou=people,dc=example,dc=com"
|
||||
Environment=SEMAPHORE_LDAP_SEARCH_FILTER="(|(uid=%[1]s)(mail=%[1]s))"
|
||||
Environment=SEMAPHORE_LDAP_MAPPING_UID="uid"
|
||||
Environment=SEMAPHORE_LDAP_MAPPING_CN="cn"
|
||||
Environment=SEMAPHORE_LDAP_MAPPING_MAIL="mail"
|
||||
Environment=SEMAPHORE_LDAP_MAPPING_DN="dn"
|
||||
```
|
||||
|
||||
You can log in with username or email.
|
||||
@@ -48,3 +48,13 @@ To integrate with LLDAP,
|
||||
allow-invalid-certs = true
|
||||
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=?)))"
|
||||
```
|
||||
|
||||
126
example_configs/truenas-install.md
Normal file
126
example_configs/truenas-install.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# Installing and Configuring LLDAP on TrueNAS
|
||||
|
||||
This guide walks through installing **LLDAP** from the TrueNAS Apps catalog and performing a basic configuration suitable for sharing authentication between multiple applications that support LDAP authentication.
|
||||
|
||||
It is intended to accompany the example configuration files in this repository and assumes a basic familiarity with the TrueNAS web interface.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- TrueNAS SCALE with Apps enabled
|
||||
- Administrative access to the TrueNAS UI
|
||||
- A system with working networking and DNS
|
||||
- Optional but recommended: HTTPS certificates managed by TrueNAS
|
||||
|
||||
## Step 1: Install LLDAP from the TrueNAS Apps Catalog
|
||||
|
||||
1. Log in to the **TrueNAS web interface**.
|
||||
2. Navigate to **Apps → Discover Apps**.
|
||||
3. Search for **LLDAP**.
|
||||
4. Click **Install**.
|
||||
|
||||
You will be presented with the LLDAP application configuration form.
|
||||
|
||||
## Step 2: Application Configuration
|
||||
|
||||
Below are the key configuration sections and recommended settings based on the official catalog definition.
|
||||
|
||||
### Application Name
|
||||
|
||||
- Leave the default name or choose a descriptive one (e.g. `lldap`).
|
||||
|
||||
### Networking
|
||||
|
||||
- **Web Port**: Default application port is typically **30325**. There is no standard port for the LLDAP web UI; this value is configurable in TrueNAS.
|
||||
- **LDAP Port**:
|
||||
- Standard LDAP port: **389**
|
||||
- Default port configured by the TrueNAS app: **30326**
|
||||
- **LDAPS Port**:
|
||||
- Standard LDAPS port: **636**
|
||||
- Default port configured by the TrueNAS app: **30327**
|
||||
|
||||
It is recommended to adjust these ports to suit your environment. Using standard ports (389/636) can simplify client configuration, but non-standard ports may be preferred to avoid conflicts on the host system. Ensure the selected ports are not already in use.
|
||||
|
||||
If LDAPS is enabled, it is strongly recommended to **disable the LDAP port** to ensure all directory traffic is encrypted.
|
||||
|
||||
### Authentication / Admin Account
|
||||
|
||||
- **LLDAP Admin Username**: Set an admin username (e.g. `admin`).
|
||||
- **LLDAP Admin Password**: Set a strong password. This account is used to access the LLDAP web UI.
|
||||
|
||||
> ⚠️ Save this password securely. You will need it to log in and manage users and groups.
|
||||
|
||||
### Base DN Configuration
|
||||
|
||||
These values define your LDAP directory structure:
|
||||
|
||||
- **Base DN**: Example: `dc=example,dc=com`
|
||||
- **User DN**: Typically `ou=people,dc=example,dc=com`
|
||||
- **Group DN**: Typically `ou=groups,dc=example,dc=com`
|
||||
|
||||
These values must be consistent with the configuration used by client applications.
|
||||
|
||||
## Step 3: Storage Configuration
|
||||
|
||||
LLDAP requires persistent storage for its database.
|
||||
|
||||
- Configure an **application dataset** or **host path** for LLDAP data.
|
||||
- Ensure the dataset is backed up as part of your normal TrueNAS backup strategy.
|
||||
|
||||
## Step 4: (Optional) Enable HTTPS Using TrueNAS Certificates
|
||||
|
||||
If your TrueNAS system manages certificates:
|
||||
|
||||
1. In the app configuration, select **Use Existing Certificate**.
|
||||
2. Choose a certificate issued by TrueNAS.
|
||||
3. Ensure the web port is accessed via `https://`.
|
||||
|
||||
This avoids storing certificate files inside the container and improves overall security.
|
||||
|
||||
## Step 5: Deploy the App
|
||||
|
||||
1. Review all configuration values.
|
||||
2. Click **Install**.
|
||||
3. Wait for the application status to show **Running**.
|
||||
|
||||
## Step 6: Access the LLDAP Web UI
|
||||
|
||||
- Navigate to: `http(s)://<truenas-ip>:<web-port>`
|
||||
- Log in using the admin credentials you configured earlier.
|
||||
|
||||
From here you can:
|
||||
- Create users
|
||||
- Create groups
|
||||
- Assign users to groups
|
||||
|
||||
## Step 7: Using LLDAP with Other Applications
|
||||
|
||||
LLDAP can be used as a central identity provider for many popular applications available in the TrueNAS Apps catalog. Common examples include:
|
||||
|
||||
- **Jellyfin** (media server)
|
||||
- **Nextcloud** (collaboration and file sharing)
|
||||
- **Gitea** (self-hosted Git service)
|
||||
- **Grafana** (monitoring and dashboards)
|
||||
- **MinIO** (object storage)
|
||||
|
||||
Configuration examples for several of these applications are also available in the upstream LLDAP repository under `example_configs`.
|
||||
|
||||
When configuring a client application:
|
||||
|
||||
- **LDAP Host**: TrueNAS IP address or the LLDAP app service name
|
||||
- **LDAP / LDAPS Port**: As configured during install (prefer LDAPS if enabled)
|
||||
- **Bind DN**: A dedicated service (bind) account or admin DN
|
||||
- **Bind Password**: Password for the bind account
|
||||
- **Base DN**: Must match the LLDAP Base DN
|
||||
|
||||
Once configured, users can authenticate to multiple applications using a single set of credentials managed centrally by LLDAP.
|
||||
|
||||
## Notes and Tips
|
||||
|
||||
- Prefer creating a **dedicated bind user** for applications instead of using the admin account.
|
||||
- Keep Base DN values consistent across all services.
|
||||
- Back up the LLDAP dataset regularly.
|
||||
|
||||
## References
|
||||
|
||||
- [TrueNAS Apps Catalog](https://apps.truenas.com/catalog/lldap/)
|
||||
- [TrueNAS SCALE Documentation](https://www.truenas.com/docs/scale/)
|
||||
@@ -159,3 +159,16 @@ key_seed = "RanD0m STR1ng"
|
||||
#cert_file="/data/cert.pem"
|
||||
## Certificate key file.
|
||||
#key_file="/data/key.pem"
|
||||
|
||||
## Options to configure the healthcheck command.
|
||||
## To set these options from environment variables, use the following format
|
||||
## (example with http_host): LLDAP_HEALTHCHECK_OPTIONS__HTTP_HOST
|
||||
[healthcheck_options]
|
||||
## The host address that the healthcheck should verify for the HTTP server.
|
||||
## If "http_host" is set to a specific IP address, this must be set to match if the built-in
|
||||
## healthcheck command is used. Note: if this is an IPv6 address, it must be wrapped in [].
|
||||
#http_host = "localhost"
|
||||
## The host address that the healthcheck should verify for the LDAP server.
|
||||
## If "ldap_host" is set to a specific IP address, this must be set to match if the built-in
|
||||
## healthcheck command is used.
|
||||
#ldap_host = "localhost"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lldap_migration_tool"
|
||||
version = "0.4.2"
|
||||
version = "0.4.3"
|
||||
description = "CLI migration tool to go from OpenLDAP to LLDAP"
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
@@ -10,31 +10,24 @@ repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = "*"
|
||||
base64 = "0.13"
|
||||
rand = "0.8"
|
||||
requestty = "0.4.1"
|
||||
serde_json = "1"
|
||||
smallvec = "*"
|
||||
anyhow = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
ldap3 = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
requestty = "0.6"
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
smallvec = "1"
|
||||
|
||||
[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]
|
||||
version = "*"
|
||||
workspace = true
|
||||
default-features = false
|
||||
features = ["json", "blocking", "rustls-tls"]
|
||||
|
||||
[dependencies.ldap3]
|
||||
version = "*"
|
||||
default-features = false
|
||||
features = ["sync", "tls-rustls"]
|
||||
|
||||
[dependencies.serde]
|
||||
workspace = true
|
||||
|
||||
@@ -193,7 +193,9 @@ impl TryFrom<ResultEntry> for User {
|
||||
display_name,
|
||||
first_name,
|
||||
last_name,
|
||||
avatar: avatar.map(base64::encode),
|
||||
avatar: avatar.map(|avatar| {
|
||||
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, avatar)
|
||||
}),
|
||||
attributes: None,
|
||||
},
|
||||
password,
|
||||
|
||||
261
schema.graphql
generated
261
schema.graphql
generated
@@ -1,63 +1,33 @@
|
||||
type AttributeValue {
|
||||
name: String!
|
||||
value: [String!]!
|
||||
schema: AttributeSchema!
|
||||
schema {
|
||||
query: Query
|
||||
mutation: Mutation
|
||||
}
|
||||
|
||||
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!
|
||||
enum AttributeType {
|
||||
STRING
|
||||
INTEGER
|
||||
JPEG_PHOTO
|
||||
DATE_TIME
|
||||
}
|
||||
|
||||
type Group {
|
||||
id: Int!
|
||||
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!
|
||||
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!
|
||||
"User-defined attributes." attributes: [AttributeValueInput!]
|
||||
}
|
||||
|
||||
"The details required to create a user."
|
||||
@@ -80,19 +50,36 @@ input CreateUserInput {
|
||||
"Attributes." attributes: [AttributeValueInput!]
|
||||
}
|
||||
|
||||
type ObjectClassInfo {
|
||||
objectClass: String!
|
||||
isHardcoded: Boolean!
|
||||
input EqualityConstraint {
|
||||
field: String!
|
||||
value: String!
|
||||
}
|
||||
|
||||
type AttributeSchema {
|
||||
name: String!
|
||||
attributeType: AttributeType!
|
||||
isList: Boolean!
|
||||
isVisible: Boolean!
|
||||
isEditable: Boolean!
|
||||
isHardcoded: Boolean!
|
||||
isReadonly: 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!]
|
||||
}
|
||||
|
||||
"The fields that can be updated for a user."
|
||||
@@ -122,9 +109,87 @@ input UpdateUserInput {
|
||||
""" insertAttributes: [AttributeValueInput!]
|
||||
}
|
||||
|
||||
input EqualityConstraint {
|
||||
field: String!
|
||||
value: String!
|
||||
"""
|
||||
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!
|
||||
}
|
||||
|
||||
type Schema {
|
||||
@@ -132,38 +197,8 @@ type Schema {
|
||||
groupSchema: AttributeList!
|
||||
}
|
||||
|
||||
"The fields that can be updated for a group."
|
||||
input UpdateGroupInput {
|
||||
"The group ID." id: Int!
|
||||
"The new display name." displayName: String
|
||||
"""
|
||||
Attribute names to remove.
|
||||
They are processed before insertions.
|
||||
""" removeAttributes: [String!]
|
||||
"""
|
||||
Inserts or updates the given attributes.
|
||||
For lists, the entire list must be provided.
|
||||
""" insertAttributes: [AttributeValueInput!]
|
||||
}
|
||||
|
||||
input AttributeValueInput {
|
||||
"""
|
||||
The name of the attribute. It must be present in the schema, and the type informs how
|
||||
to interpret the values.
|
||||
""" name: String!
|
||||
"""
|
||||
The values of the attribute.
|
||||
If the attribute is not a list, the vector must contain exactly one element.
|
||||
Integers (signed 64 bits) are represented as strings.
|
||||
Dates are represented as strings in RFC3339 format, e.g. "2019-10-12T07:20:50.52Z".
|
||||
JpegPhotos are represented as base64 encoded strings. They must be valid JPEGs.
|
||||
""" value: [String!]!
|
||||
}
|
||||
|
||||
"The details required to create a group."
|
||||
input CreateGroupInput {
|
||||
displayName: String!
|
||||
"User-defined attributes." attributes: [AttributeValueInput!]
|
||||
type Success {
|
||||
ok: Boolean!
|
||||
}
|
||||
|
||||
type User {
|
||||
@@ -173,32 +208,10 @@ type User {
|
||||
firstName: String!
|
||||
lastName: String!
|
||||
avatar: String
|
||||
creationDate: DateTimeUtc!
|
||||
creationDate: DateTime!
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lldap"
|
||||
version = "0.6.2"
|
||||
version = "0.6.3"
|
||||
description = "Super-simple and lightweight LDAP server"
|
||||
categories = ["authentication", "command-line-utilities"]
|
||||
edition.workspace = true
|
||||
@@ -18,63 +18,69 @@ actix-http = "3"
|
||||
actix-rt = "2"
|
||||
actix-server = "2"
|
||||
actix-service = "2"
|
||||
actix-web = "4.3"
|
||||
actix-web-httpauth = "0.8"
|
||||
anyhow = "*"
|
||||
async-trait = "0.1"
|
||||
bincode = "1.3"
|
||||
cron = "*"
|
||||
derive_builder = "0.12"
|
||||
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"
|
||||
figment_file_provider_adapter = "0.1"
|
||||
futures = "*"
|
||||
futures-util = "*"
|
||||
futures = "0"
|
||||
futures-util = "0"
|
||||
hmac = "0.12"
|
||||
http = "*"
|
||||
juniper = "0.15"
|
||||
jwt = "0.16"
|
||||
ldap3_proto = "0.6.0"
|
||||
log = "*"
|
||||
juniper = { workspace = true }
|
||||
jwt = { workspace = true }
|
||||
ldap3_proto = { workspace = true }
|
||||
log = { workspace = true }
|
||||
opaque-ke = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
rand_chacha = "0.3"
|
||||
rustls-pemfile = "1"
|
||||
serde_json = "1"
|
||||
rustls-pemfile = "2"
|
||||
sea-orm = { workspace = true }
|
||||
secstr = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
sha2 = "0.10"
|
||||
thiserror = "2"
|
||||
strum = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
time = "0.3"
|
||||
tokio-rustls = "0.23"
|
||||
tokio-stream = "*"
|
||||
tokio = { workspace = true }
|
||||
tokio-rustls = "0.26"
|
||||
tokio-stream = "0"
|
||||
tokio-util = "0.7"
|
||||
tracing = "*"
|
||||
tracing = { workspace = true }
|
||||
tracing-actix-web = "0.7"
|
||||
tracing-attributes = "^0.1.21"
|
||||
tracing-log = "*"
|
||||
urlencoding = "2"
|
||||
webpki-roots = "0.22.2"
|
||||
tracing-log = "0"
|
||||
tracing-subscriber = { workspace = true }
|
||||
urlencoding = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
webpki-roots = "1"
|
||||
|
||||
[dependencies.chrono]
|
||||
features = ["serde"]
|
||||
version = "*"
|
||||
[dependencies.actix-web]
|
||||
features = ["rustls-0_23"]
|
||||
version = "4.12.1"
|
||||
|
||||
[dependencies.clap]
|
||||
features = ["std", "color", "suggestions", "derive", "env"]
|
||||
version = "4"
|
||||
|
||||
[dependencies.derive_more]
|
||||
features = ["debug", "display", "from", "from_str"]
|
||||
[dependencies.rustls]
|
||||
default-features = false
|
||||
features = ["ring", "logging", "std", "tls12"]
|
||||
version = "0.23"
|
||||
|
||||
[dependencies.rustls-pki-types]
|
||||
features = ["std"]
|
||||
version = "1"
|
||||
|
||||
[dependencies.figment]
|
||||
features = ["env", "toml"]
|
||||
version = "*"
|
||||
|
||||
[dependencies.tracing-subscriber]
|
||||
version = "0.3"
|
||||
features = ["env-filter", "tracing-log"]
|
||||
version = "0"
|
||||
|
||||
[dependencies.lettre]
|
||||
features = ["builder", "serde", "smtp-transport", "tokio1-rustls-tls"]
|
||||
default-features = false
|
||||
version = "0.10.1"
|
||||
version = "0.11.19"
|
||||
|
||||
[dependencies.lldap_access_control]
|
||||
path = "../crates/access-control"
|
||||
@@ -114,78 +120,31 @@ 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.1.6"
|
||||
version = "0.3"
|
||||
|
||||
[dependencies.actix-tls]
|
||||
features = ["default", "rustls"]
|
||||
features = ["default", "rustls-0_23"]
|
||||
version = "3"
|
||||
|
||||
[dependencies.sea-orm]
|
||||
workspace = true
|
||||
features = [
|
||||
"macros",
|
||||
"with-chrono",
|
||||
"with-uuid",
|
||||
"sqlx-all",
|
||||
"runtime-actix-rustls",
|
||||
]
|
||||
|
||||
[dependencies.reqwest]
|
||||
version = "0.11"
|
||||
workspace = true
|
||||
default-features = false
|
||||
features = ["rustls-tls-webpki-roots"]
|
||||
|
||||
[dependencies.rustls]
|
||||
version = "0.20"
|
||||
features = ["dangerous_configuration"]
|
||||
|
||||
[dependencies.url]
|
||||
version = "2"
|
||||
features = ["serde"]
|
||||
|
||||
[dev-dependencies]
|
||||
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"]
|
||||
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 }
|
||||
|
||||
[dev-dependencies.lldap_auth]
|
||||
path = "../crates/auth"
|
||||
@@ -203,7 +162,7 @@ path = "../crates/sql-backend-handler"
|
||||
features = ["test"]
|
||||
|
||||
[dev-dependencies.reqwest]
|
||||
version = "*"
|
||||
workspace = true
|
||||
default-features = false
|
||||
features = ["json", "blocking", "rustls-tls"]
|
||||
|
||||
@@ -212,10 +171,6 @@ version = "2.0.0"
|
||||
default-features = false
|
||||
features = ["file_locks"]
|
||||
|
||||
[dev-dependencies.uuid]
|
||||
version = "1"
|
||||
features = ["v4"]
|
||||
|
||||
[dev-dependencies.figment]
|
||||
features = ["test"]
|
||||
version = "*"
|
||||
version = "0"
|
||||
|
||||
@@ -174,6 +174,9 @@ pub struct RunOpts {
|
||||
|
||||
#[clap(flatten)]
|
||||
pub ldaps_opts: LdapsOpts,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub healthcheck_opts: HealthcheckOpts,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser, Clone)]
|
||||
@@ -264,6 +267,18 @@ pub struct ExportGraphQLSchemaOpts {
|
||||
pub output_file: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser, Clone)]
|
||||
#[clap(next_help_heading = Some("HEALTHCHECK"))]
|
||||
pub struct HealthcheckOpts {
|
||||
/// Change the HTTP Host to test the health of. Default: "localhost"
|
||||
#[clap(long, env = "LLDAP_HEALTHCHECK_OPTIONS__HTTP_HOST")]
|
||||
pub healthcheck_http_host: Option<String>,
|
||||
|
||||
/// Change the LDAP Host to test the health of. Default: "localhost"
|
||||
#[clap(long, env = "LLDAP_HEALTHCHECK_OPTIONS__LDAP_HOST")]
|
||||
pub healthcheck_ldap_host: Option<String>,
|
||||
}
|
||||
|
||||
pub fn init() -> CLIOpts {
|
||||
CLIOpts::parse()
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
cli::{
|
||||
GeneralConfigOpts, LdapsOpts, RunOpts, SmtpEncryption, SmtpOpts, TestEmailOpts,
|
||||
TrueFalseAlways,
|
||||
GeneralConfigOpts, HealthcheckOpts, LdapsOpts, RunOpts, SmtpEncryption, SmtpOpts,
|
||||
TestEmailOpts, TrueFalseAlways,
|
||||
},
|
||||
database_string::DatabaseUrl,
|
||||
};
|
||||
@@ -83,6 +83,21 @@ impl std::default::Default for LdapsOptions {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, derive_builder::Builder)]
|
||||
#[builder(pattern = "owned")]
|
||||
pub struct HealthcheckOptions {
|
||||
#[builder(default = r#"String::from("localhost")"#)]
|
||||
pub http_host: String,
|
||||
#[builder(default = r#"String::from("localhost")"#)]
|
||||
pub ldap_host: String,
|
||||
}
|
||||
|
||||
impl std::default::Default for HealthcheckOptions {
|
||||
fn default() -> Self {
|
||||
HealthcheckOptionsBuilder::default().build().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize, derive_more::Debug)]
|
||||
#[debug(r#""{_0}""#)]
|
||||
pub struct HttpUrl(pub Url);
|
||||
@@ -138,6 +153,8 @@ pub struct Configuration {
|
||||
#[serde(skip)]
|
||||
#[builder(field(private), default = "None")]
|
||||
server_setup: Option<ServerSetupConfig>,
|
||||
#[builder(default)]
|
||||
pub healthcheck_options: HealthcheckOptions,
|
||||
}
|
||||
|
||||
impl std::default::Default for Configuration {
|
||||
@@ -523,6 +540,18 @@ impl ConfigOverrider for SmtpOpts {
|
||||
}
|
||||
}
|
||||
|
||||
impl ConfigOverrider for HealthcheckOpts {
|
||||
fn override_config(&self, config: &mut Configuration) {
|
||||
self.healthcheck_http_host
|
||||
.as_ref()
|
||||
.inspect(|host| config.healthcheck_options.http_host.clone_from(host));
|
||||
|
||||
self.healthcheck_ldap_host
|
||||
.as_ref()
|
||||
.inspect(|host| config.healthcheck_options.ldap_host.clone_from(host));
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_keys(dict: &figment::value::Dict) -> HashSet<String> {
|
||||
use figment::value::{Dict, Value};
|
||||
fn process_value(value: &Dict, keys: &mut HashSet<String>, path: &mut Vec<String>) {
|
||||
|
||||
@@ -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<'static, Query, Mutation, Subscription, S>,
|
||||
schema: &juniper::RootNode<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<'static, Query, Mutation, Subscription, S>,
|
||||
schema: &juniper::RootNode<Query, Mutation, Subscription, S>,
|
||||
context: &CtxT,
|
||||
req: HttpRequest,
|
||||
mut payload: actix_http::Payload,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{configuration::LdapsOptions, ldap_server::read_certificates};
|
||||
use crate::{configuration::LdapsOptions, tls};
|
||||
use anyhow::{Context, Result, anyhow, bail, ensure};
|
||||
use futures_util::SinkExt;
|
||||
use ldap3_proto::{
|
||||
@@ -8,6 +8,11 @@ use ldap3_proto::{
|
||||
LdapSearchScope,
|
||||
},
|
||||
};
|
||||
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
|
||||
use rustls::crypto::{verify_tls12_signature, verify_tls13_signature};
|
||||
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
||||
use rustls::{DigitallySignedStruct, SignatureScheme};
|
||||
use std::sync::Arc;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_rustls::TlsConnector as RustlsTlsConnector;
|
||||
use tokio_util::codec::{FramedRead, FramedWrite};
|
||||
@@ -70,77 +75,107 @@ where
|
||||
}
|
||||
|
||||
#[instrument(level = "info", err)]
|
||||
pub async fn check_ldap(port: u16) -> Result<()> {
|
||||
check_ldap_endpoint(TcpStream::connect(format!("localhost:{port}")).await?).await
|
||||
}
|
||||
|
||||
fn get_root_certificates() -> rustls::RootCertStore {
|
||||
let mut root_store = rustls::RootCertStore::empty();
|
||||
root_store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.iter().map(|ta| {
|
||||
rustls::OwnedTrustAnchor::from_subject_spki_name_constraints(
|
||||
ta.subject,
|
||||
ta.spki,
|
||||
ta.name_constraints,
|
||||
)
|
||||
}));
|
||||
root_store
|
||||
pub async fn check_ldap(host: &str, port: u16) -> Result<()> {
|
||||
check_ldap_endpoint(TcpStream::connect((host, port)).await?).await
|
||||
}
|
||||
|
||||
fn get_tls_connector(ldaps_options: &LdapsOptions) -> Result<RustlsTlsConnector> {
|
||||
let mut client_config = rustls::ClientConfig::builder()
|
||||
.with_safe_defaults()
|
||||
.with_root_certificates(get_root_certificates())
|
||||
.with_no_client_auth();
|
||||
let (certs, _private_key) = read_certificates(ldaps_options)?;
|
||||
// Check that the server cert is the one in the config file.
|
||||
let certs = tls::load_certificates(&ldaps_options.cert_file)?;
|
||||
let target_cert = certs.first().expect("empty certificate chain").clone();
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CertificateVerifier {
|
||||
certificate: rustls::Certificate,
|
||||
certificate_path: String,
|
||||
certificate: CertificateDer<'static>,
|
||||
}
|
||||
impl rustls::client::ServerCertVerifier for CertificateVerifier {
|
||||
|
||||
impl ServerCertVerifier for CertificateVerifier {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
end_entity: &rustls::Certificate,
|
||||
_intermediates: &[rustls::Certificate],
|
||||
_server_name: &rustls::ServerName,
|
||||
_scts: &mut dyn Iterator<Item = &[u8]>,
|
||||
end_entity: &CertificateDer<'_>,
|
||||
_intermediates: &[CertificateDer<'_>],
|
||||
_server_name: &ServerName<'_>,
|
||||
_ocsp_response: &[u8],
|
||||
_now: std::time::SystemTime,
|
||||
) -> std::result::Result<rustls::client::ServerCertVerified, rustls::Error> {
|
||||
_now: UnixTime,
|
||||
) -> Result<ServerCertVerified, rustls::Error> {
|
||||
if end_entity != &self.certificate {
|
||||
return Err(rustls::Error::InvalidCertificateData(format!(
|
||||
"Server certificate doesn't match the one in the config file {}",
|
||||
&self.certificate_path
|
||||
)));
|
||||
return Err(rustls::Error::InvalidCertificate(
|
||||
rustls::CertificateError::NotValidForName,
|
||||
));
|
||||
}
|
||||
Ok(rustls::client::ServerCertVerified::assertion())
|
||||
Ok(ServerCertVerified::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
message: &[u8],
|
||||
cert: &CertificateDer<'_>,
|
||||
dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, rustls::Error> {
|
||||
verify_tls12_signature(
|
||||
message,
|
||||
cert,
|
||||
dss,
|
||||
&rustls::crypto::ring::default_provider().signature_verification_algorithms,
|
||||
)
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
message: &[u8],
|
||||
cert: &CertificateDer<'_>,
|
||||
dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, rustls::Error> {
|
||||
verify_tls13_signature(
|
||||
message,
|
||||
cert,
|
||||
dss,
|
||||
&rustls::crypto::ring::default_provider().signature_verification_algorithms,
|
||||
)
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
||||
rustls::crypto::ring::default_provider()
|
||||
.signature_verification_algorithms
|
||||
.supported_schemes()
|
||||
}
|
||||
}
|
||||
let mut dangerous_config = rustls::client::DangerousClientConfig {
|
||||
cfg: &mut client_config,
|
||||
};
|
||||
dangerous_config.set_certificate_verifier(std::sync::Arc::new(CertificateVerifier {
|
||||
certificate: certs.first().expect("empty certificate chain").clone(),
|
||||
certificate_path: ldaps_options.cert_file.clone(),
|
||||
}));
|
||||
Ok(std::sync::Arc::new(client_config).into())
|
||||
|
||||
let verifier = Arc::new(CertificateVerifier {
|
||||
certificate: target_cert,
|
||||
});
|
||||
|
||||
let client_config = rustls::ClientConfig::builder_with_provider(
|
||||
rustls::crypto::ring::default_provider().into(),
|
||||
)
|
||||
.with_safe_default_protocol_versions()
|
||||
.context("Failed to set default protocol versions")?
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(verifier)
|
||||
.with_no_client_auth();
|
||||
|
||||
Ok(Arc::new(client_config).into())
|
||||
}
|
||||
|
||||
#[instrument(skip_all, level = "info", err, fields(port = %ldaps_options.port))]
|
||||
pub async fn check_ldaps(ldaps_options: &LdapsOptions) -> Result<()> {
|
||||
#[instrument(skip_all, level = "info", err, fields(host = %host, port = %ldaps_options.port))]
|
||||
pub async fn check_ldaps(host: &str, ldaps_options: &LdapsOptions) -> Result<()> {
|
||||
if !ldaps_options.enabled {
|
||||
info!("LDAPS not enabled");
|
||||
return Ok(());
|
||||
};
|
||||
let tls_connector =
|
||||
get_tls_connector(ldaps_options).context("while preparing the tls connection")?;
|
||||
let url = format!("localhost:{}", ldaps_options.port);
|
||||
|
||||
let domain = match host.parse::<std::net::IpAddr>() {
|
||||
Ok(ip) => ServerName::IpAddress(ip.into()),
|
||||
Err(_) => ServerName::try_from(host.to_string())
|
||||
.map_err(|_| anyhow!("Invalid DNS name: {}", host))?,
|
||||
};
|
||||
|
||||
check_ldap_endpoint(
|
||||
tls_connector
|
||||
.connect(
|
||||
rustls::ServerName::try_from("localhost")
|
||||
.context("while parsing the server name")?,
|
||||
TcpStream::connect(&url)
|
||||
domain,
|
||||
TcpStream::connect((host, ldaps_options.port))
|
||||
.await
|
||||
.context("while connecting TCP")?,
|
||||
)
|
||||
@@ -151,8 +186,8 @@ pub async fn check_ldaps(ldaps_options: &LdapsOptions) -> Result<()> {
|
||||
}
|
||||
|
||||
#[instrument(level = "info", err)]
|
||||
pub async fn check_api(port: u16) -> Result<()> {
|
||||
reqwest::get(format!("http://localhost:{port}/health"))
|
||||
pub async fn check_api(host: &str, port: u16) -> Result<()> {
|
||||
reqwest::get(format!("http://{host}:{port}/health"))
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
info!("Success");
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use crate::configuration::{Configuration, LdapsOptions};
|
||||
use crate::tls;
|
||||
use actix_rt::net::TcpStream;
|
||||
use actix_server::ServerBuilder;
|
||||
use actix_service::{ServiceFactoryExt, fn_service};
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use anyhow::{Context, Result};
|
||||
use ldap3_proto::{LdapCodec, control::LdapControl, proto::LdapMsg, proto::LdapOp};
|
||||
use lldap_access_control::AccessControlledBackendHandler;
|
||||
use lldap_domain_handlers::handler::{BackendHandler, LoginHandler};
|
||||
use lldap_ldap::{LdapHandler, LdapInfo};
|
||||
use lldap_opaque_handler::OpaqueHandler;
|
||||
use rustls::PrivateKey;
|
||||
use tokio_rustls::TlsAcceptor as RustlsTlsAcceptor;
|
||||
use tokio_util::codec::{FramedRead, FramedWrite};
|
||||
use tracing::{debug, error, info, instrument};
|
||||
@@ -102,55 +102,18 @@ where
|
||||
Ok(requests.into_inner().unsplit(resp.into_inner()))
|
||||
}
|
||||
|
||||
fn read_private_key(key_file: &str) -> Result<PrivateKey> {
|
||||
use rustls_pemfile::{ec_private_keys, pkcs8_private_keys, rsa_private_keys};
|
||||
use std::{fs::File, io::BufReader};
|
||||
pkcs8_private_keys(&mut BufReader::new(File::open(key_file)?))
|
||||
.map_err(anyhow::Error::from)
|
||||
.and_then(|keys| {
|
||||
keys.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("No PKCS8 key"))
|
||||
})
|
||||
.or_else(|_| {
|
||||
rsa_private_keys(&mut BufReader::new(File::open(key_file)?))
|
||||
.map_err(anyhow::Error::from)
|
||||
.and_then(|keys| {
|
||||
keys.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("No PKCS1 key"))
|
||||
})
|
||||
})
|
||||
.or_else(|_| {
|
||||
ec_private_keys(&mut BufReader::new(File::open(key_file)?))
|
||||
.map_err(anyhow::Error::from)
|
||||
.and_then(|keys| keys.into_iter().next().ok_or_else(|| anyhow!("No EC key")))
|
||||
})
|
||||
.with_context(|| {
|
||||
format!("Cannot read either PKCS1, PKCS8 or EC private key from {key_file}")
|
||||
})
|
||||
.map(rustls::PrivateKey)
|
||||
}
|
||||
|
||||
pub fn read_certificates(
|
||||
ldaps_options: &LdapsOptions,
|
||||
) -> Result<(Vec<rustls::Certificate>, rustls::PrivateKey)> {
|
||||
use std::{fs::File, io::BufReader};
|
||||
let certs = rustls_pemfile::certs(&mut BufReader::new(File::open(&ldaps_options.cert_file)?))?
|
||||
.into_iter()
|
||||
.map(rustls::Certificate)
|
||||
.collect::<Vec<_>>();
|
||||
let private_key = read_private_key(&ldaps_options.key_file)?;
|
||||
Ok((certs, private_key))
|
||||
}
|
||||
|
||||
fn get_tls_acceptor(ldaps_options: &LdapsOptions) -> Result<RustlsTlsAcceptor> {
|
||||
let (certs, private_key) = read_certificates(ldaps_options)?;
|
||||
let certs = tls::load_certificates(&ldaps_options.cert_file)?;
|
||||
let private_key = tls::load_private_key(&ldaps_options.key_file)?;
|
||||
|
||||
let server_config = std::sync::Arc::new(
|
||||
rustls::ServerConfig::builder()
|
||||
.with_safe_defaults()
|
||||
.with_no_client_auth()
|
||||
.with_single_cert(certs, private_key)?,
|
||||
rustls::ServerConfig::builder_with_provider(
|
||||
rustls::crypto::ring::default_provider().into(),
|
||||
)
|
||||
.with_safe_default_protocol_versions()
|
||||
.context("Failed to set default protocol versions")?
|
||||
.with_no_client_auth()
|
||||
.with_single_cert(certs, private_key)?,
|
||||
);
|
||||
Ok(server_config.into())
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ mod mail;
|
||||
mod sql_tcp_backend_handler;
|
||||
mod tcp_backend_handler;
|
||||
mod tcp_server;
|
||||
mod tls;
|
||||
|
||||
use crate::{
|
||||
cli::{Command, RunOpts, TestEmailOpts},
|
||||
@@ -255,9 +256,18 @@ async fn run_healthcheck(opts: RunOpts) -> Result<()> {
|
||||
use tokio::time::timeout;
|
||||
let delay = Duration::from_millis(3000);
|
||||
let (ldap, ldaps, api) = tokio::join!(
|
||||
timeout(delay, healthcheck::check_ldap(config.ldap_port)),
|
||||
timeout(delay, healthcheck::check_ldaps(&config.ldaps_options)),
|
||||
timeout(delay, healthcheck::check_api(config.http_port)),
|
||||
timeout(
|
||||
delay,
|
||||
healthcheck::check_ldap(&config.healthcheck_options.ldap_host, config.ldap_port)
|
||||
),
|
||||
timeout(
|
||||
delay,
|
||||
healthcheck::check_ldaps(&config.healthcheck_options.ldap_host, &config.ldaps_options)
|
||||
),
|
||||
timeout(
|
||||
delay,
|
||||
healthcheck::check_api(&config.healthcheck_options.http_host, config.http_port)
|
||||
),
|
||||
);
|
||||
|
||||
let failure = [ldap, ldaps, api]
|
||||
|
||||
@@ -12,3 +12,4 @@ pub mod mail;
|
||||
pub mod sql_tcp_backend_handler;
|
||||
pub mod tcp_backend_handler;
|
||||
pub mod tcp_server;
|
||||
pub mod tls;
|
||||
|
||||
20
server/src/tls.rs
Normal file
20
server/src/tls.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use rustls::pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject};
|
||||
|
||||
pub fn load_certificates(filename: &str) -> Result<Vec<CertificateDer<'static>>> {
|
||||
let certs = CertificateDer::pem_file_iter(filename)
|
||||
.with_context(|| format!("Unable to open or read certificate file: {}", filename))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.with_context(|| format!("Error parsing certificates in {}", filename))?;
|
||||
|
||||
if certs.is_empty() {
|
||||
return Err(anyhow!("No certificates found in {}", filename));
|
||||
}
|
||||
|
||||
Ok(certs)
|
||||
}
|
||||
|
||||
pub fn load_private_key(filename: &str) -> Result<PrivateKeyDer<'static>> {
|
||||
PrivateKeyDer::from_pem_file(filename)
|
||||
.with_context(|| format!("Unable to load private key from {}", filename))
|
||||
}
|
||||
@@ -6,7 +6,7 @@ use crate::common::{
|
||||
add_user_to_group, create_group, create_user, delete_group_query, delete_user_query, post,
|
||||
},
|
||||
};
|
||||
use assert_cmd::prelude::*;
|
||||
use assert_cmd::cargo_bin;
|
||||
use nix::{
|
||||
sys::signal::{self, Signal},
|
||||
unistd::Pid,
|
||||
@@ -226,7 +226,7 @@ pub fn new_id(prefix: Option<&str>) -> String {
|
||||
}
|
||||
|
||||
fn create_lldap_command(subcommand: &str) -> Command {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).expect("cargo bin not found");
|
||||
let mut cmd = Command::new(cargo_bin!());
|
||||
// This gives us the absolute path of the repo base instead of running it in server/
|
||||
let path = canonicalize("..").expect("canonical path");
|
||||
let db_url = env::database_url();
|
||||
|
||||
@@ -4,7 +4,8 @@ use anyhow::{Context, Result, anyhow};
|
||||
use graphql_client::GraphQLQuery;
|
||||
use reqwest::blocking::Client;
|
||||
|
||||
pub type DateTimeUtc = chrono::DateTime<chrono::Utc>;
|
||||
pub type DateTime = chrono::DateTime<chrono::Utc>;
|
||||
pub type DateTimeUtc = DateTime;
|
||||
|
||||
#[derive(GraphQLQuery)]
|
||||
#[graphql(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lldap_set_password"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
description = "CLI tool to set a user password in LLDAP"
|
||||
edition.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
|
||||
|
||||
[dependencies]
|
||||
anyhow = "*"
|
||||
rand = "0.8"
|
||||
serde_json = "1"
|
||||
anyhow = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[dependencies.clap]
|
||||
features = ["std", "color", "suggestions", "derive", "env"]
|
||||
version = "4"
|
||||
clap = { workspace = true }
|
||||
|
||||
[dependencies.lldap_auth]
|
||||
path = "../crates/auth"
|
||||
features = ["opaque_client"]
|
||||
|
||||
[dependencies.reqwest]
|
||||
version = "*"
|
||||
default-features = false
|
||||
features = ["json", "blocking", "rustls-tls", "rustls-tls-native-roots"]
|
||||
|
||||
[dependencies.serde]
|
||||
workspace = true
|
||||
features = ["json", "blocking", "rustls-tls", "rustls-tls-native-roots"]
|
||||
|
||||
Reference in New Issue
Block a user