mirror of
https://github.com/lldap/lldap.git
synced 2026-07-13 02:53:32 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f31147bb7 |
@@ -59,7 +59,7 @@ RUN set -x \
|
|||||||
&& for file in $(cat /lldap/app/static/fonts/fonts.txt); do wget -P app/static/fonts "$file"; done \
|
&& for file in $(cat /lldap/app/static/fonts/fonts.txt); do wget -P app/static/fonts "$file"; done \
|
||||||
&& chmod a+r -R .
|
&& chmod a+r -R .
|
||||||
|
|
||||||
FROM alpine:3.22
|
FROM alpine:3.19
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ENV UID=1000
|
ENV UID=1000
|
||||||
ENV GID=1000
|
ENV GID=1000
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Keep tracking base image
|
# Keep tracking base image
|
||||||
FROM rust:1.91-slim-bookworm
|
FROM rust:1.89-slim-bookworm
|
||||||
|
|
||||||
# Set needed env path
|
# Set needed env path
|
||||||
ENV PATH="/opt/armv7l-linux-musleabihf-cross/:/opt/armv7l-linux-musleabihf-cross/bin/:/opt/aarch64-linux-musl-cross/:/opt/aarch64-linux-musl-cross/bin/:/opt/x86_64-linux-musl-cross/:/opt/x86_64-linux-musl-cross/bin/:$PATH"
|
ENV PATH="/opt/armv7l-linux-musleabihf-cross/:/opt/armv7l-linux-musleabihf-cross/bin/:/opt/aarch64-linux-musl-cross/:/opt/aarch64-linux-musl-cross/bin/:/opt/x86_64-linux-musl-cross/:/opt/x86_64-linux-musl-cross/bin/:$PATH"
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
MSRV: "1.91.0"
|
MSRV: "1.89.0"
|
||||||
|
|
||||||
### CI Docs
|
### CI Docs
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
lldap-ui-
|
lldap-ui-
|
||||||
- name: Install wasm-pack with cargo
|
- name: Install wasm-pack with cargo
|
||||||
run: cargo install --locked wasm-pack || true
|
run: cargo install wasm-pack || true
|
||||||
env:
|
env:
|
||||||
RUSTFLAGS: ""
|
RUSTFLAGS: ""
|
||||||
- name: Build frontend
|
- name: Build frontend
|
||||||
@@ -520,7 +520,7 @@ jobs:
|
|||||||
path: web
|
path: web
|
||||||
|
|
||||||
- name: Setup QEMU
|
- name: Setup QEMU
|
||||||
uses: docker/setup-qemu-action@v4
|
uses: docker/setup-qemu-action@v3
|
||||||
- name: Setup buildx
|
- name: Setup buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
with:
|
with:
|
||||||
@@ -623,14 +623,14 @@ jobs:
|
|||||||
# Docker login to nitnelave/lldap and lldap/lldap
|
# Docker login to nitnelave/lldap and lldap/lldap
|
||||||
- name: Login to Nitnelave/LLDAP Docker Hub
|
- name: Login to Nitnelave/LLDAP Docker Hub
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: nitnelave
|
username: nitnelave
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
MSRV: "1.91.0"
|
MSRV: "1.89.0"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pre_job:
|
pre_job:
|
||||||
|
|||||||
@@ -5,55 +5,6 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [0.6.3] 2026-05-01
|
|
||||||
|
|
||||||
Small release, focused on LDAP compatibility, TLS maintenance, dependency upgrades and documentation/examples.
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- LDAP schema definitions for `memberOf`, `modifyTimestamp` and `pwdChangedTime`
|
|
||||||
- Support for configuring the healthcheck listen addresses
|
|
||||||
- Usernames are now included in password recovery emails
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- JWT `exp` and `iat` claims are now serialized as NumericDate values to comply with RFC7519
|
|
||||||
- Migrated to `rustls` 0.23 and centralized TLS handling
|
|
||||||
- The login form no longer enforces a password length limit
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- `pwdChangedTime` is now emitted as LDAP GeneralizedTime instead of RFC3339
|
|
||||||
- LDAP base-scope searches for non-existent entries now return `NoSuchObject`
|
|
||||||
- `cn` equality filters are now case insensitive
|
|
||||||
- The server now shuts down the database connection pool gracefully
|
|
||||||
- The bootstrap script now handles empty globs correctly
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
- Updated the LDAP dependency stack, including `ldap3_proto`, in response to
|
|
||||||
security advisory
|
|
||||||
[`GHSA-qcxq-75wr-5cm8`](https://github.com/kanidm/ldap3/security/advisories/GHSA-qcxq-75wr-5cm8),
|
|
||||||
where a specially crafted LDAP query could make the server crash
|
|
||||||
|
|
||||||
### Cleanups
|
|
||||||
|
|
||||||
- Split GraphQL queries and mutations into smaller modules
|
|
||||||
- Refactored configuration and user update logic
|
|
||||||
- Upgraded the Rust toolchain and shared dependencies
|
|
||||||
|
|
||||||
### New services
|
|
||||||
|
|
||||||
- Apache WebDAV
|
|
||||||
- Continuwuity
|
|
||||||
- Gerrit
|
|
||||||
- Gogs
|
|
||||||
- Open WebUI
|
|
||||||
- OpenCloud
|
|
||||||
- Pocket ID
|
|
||||||
- Semaphore
|
|
||||||
- TrueNAS
|
|
||||||
|
|
||||||
## [0.6.2] 2025-07-21
|
## [0.6.2] 2025-07-21
|
||||||
|
|
||||||
Small release, focused on LDAP improvements and ongoing maintenance.
|
Small release, focused on LDAP improvements and ongoing maintenance.
|
||||||
|
|||||||
Generated
+1417
-1110
File diff suppressed because it is too large
Load Diff
+1
-114
@@ -16,7 +16,7 @@ edition = "2024"
|
|||||||
homepage = "https://github.com/lldap/lldap"
|
homepage = "https://github.com/lldap/lldap"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
repository = "https://github.com/lldap/lldap"
|
repository = "https://github.com/lldap/lldap"
|
||||||
rust-version = "1.91.0"
|
rust-version = "1.89.0"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
@@ -24,122 +24,9 @@ lto = true
|
|||||||
[profile.release.package.lldap_app]
|
[profile.release.package.lldap_app]
|
||||||
opt-level = 's'
|
opt-level = 's'
|
||||||
|
|
||||||
[workspace.dependencies.anyhow]
|
|
||||||
version = "1"
|
|
||||||
|
|
||||||
[workspace.dependencies.async-trait]
|
|
||||||
version = "0.1"
|
|
||||||
|
|
||||||
[workspace.dependencies.base64]
|
|
||||||
version = "0.22"
|
|
||||||
|
|
||||||
[workspace.dependencies.bincode]
|
|
||||||
version = "1"
|
|
||||||
|
|
||||||
[workspace.dependencies.chrono]
|
|
||||||
version = "0.4"
|
|
||||||
features = ["serde", "wasmbind"]
|
|
||||||
|
|
||||||
[workspace.dependencies.clap]
|
|
||||||
version = "4"
|
|
||||||
features = ["std", "color", "suggestions", "derive", "env"]
|
|
||||||
|
|
||||||
[workspace.dependencies.derive_more]
|
|
||||||
version = "2"
|
|
||||||
default-features = false
|
|
||||||
features = ["debug", "display", "from", "from_str"]
|
|
||||||
|
|
||||||
[workspace.dependencies.graphql_client]
|
|
||||||
version = "0.11"
|
|
||||||
default-features = false
|
|
||||||
features = ["graphql_query_derive"]
|
|
||||||
|
|
||||||
[workspace.dependencies.image]
|
|
||||||
version = "0.25"
|
|
||||||
default-features = false
|
|
||||||
features = ["jpeg"]
|
|
||||||
|
|
||||||
[workspace.dependencies.itertools]
|
|
||||||
version = "0.14"
|
|
||||||
|
|
||||||
[workspace.dependencies.juniper]
|
|
||||||
version = "0.17"
|
|
||||||
default-features = false
|
|
||||||
features = ["chrono", "schema-language", "url", "uuid"]
|
|
||||||
|
|
||||||
[workspace.dependencies.ldap3]
|
|
||||||
version = "0"
|
|
||||||
default-features = false
|
|
||||||
features = ["sync", "tls-rustls-ring"]
|
|
||||||
|
|
||||||
[workspace.dependencies.ldap3_proto]
|
|
||||||
version = "0.7"
|
|
||||||
|
|
||||||
[workspace.dependencies.jwt]
|
|
||||||
version = "0.16"
|
|
||||||
|
|
||||||
[workspace.dependencies.log]
|
|
||||||
version = "0"
|
|
||||||
|
|
||||||
[workspace.dependencies.mockall]
|
|
||||||
version = "0.14"
|
|
||||||
|
|
||||||
[workspace.dependencies.opaque-ke]
|
|
||||||
version = "0.7"
|
|
||||||
|
|
||||||
[workspace.dependencies.orion]
|
|
||||||
version = "0.17"
|
|
||||||
|
|
||||||
[workspace.dependencies.pretty_assertions]
|
|
||||||
version = "1"
|
|
||||||
|
|
||||||
[workspace.dependencies.rand]
|
|
||||||
version = "0.8"
|
|
||||||
features = ["small_rng", "getrandom"]
|
|
||||||
|
|
||||||
[workspace.dependencies.reqwest]
|
|
||||||
version = "0.11"
|
|
||||||
default-features = false
|
|
||||||
|
|
||||||
[workspace.dependencies.sea-orm]
|
[workspace.dependencies.sea-orm]
|
||||||
version = "1.1.8"
|
version = "1.1.8"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["macros", "with-chrono", "with-uuid", "sqlx-all", "runtime-actix-rustls"]
|
|
||||||
|
|
||||||
[workspace.dependencies.secstr]
|
|
||||||
version = "0"
|
|
||||||
features = ["serde"]
|
|
||||||
|
|
||||||
[workspace.dependencies.serde]
|
[workspace.dependencies.serde]
|
||||||
version = "1"
|
version = "1"
|
||||||
|
|
||||||
[workspace.dependencies.serde_bytes]
|
|
||||||
version = "0.11"
|
|
||||||
|
|
||||||
[workspace.dependencies.serde_json]
|
|
||||||
version = "1"
|
|
||||||
|
|
||||||
[workspace.dependencies.strum]
|
|
||||||
version = "0.28"
|
|
||||||
features = ["derive"]
|
|
||||||
|
|
||||||
[workspace.dependencies.thiserror]
|
|
||||||
version = "2"
|
|
||||||
|
|
||||||
[workspace.dependencies.tokio]
|
|
||||||
version = "1"
|
|
||||||
features = ["full"]
|
|
||||||
|
|
||||||
[workspace.dependencies.tracing]
|
|
||||||
version = "0"
|
|
||||||
|
|
||||||
[workspace.dependencies.tracing-subscriber]
|
|
||||||
version = "0.3"
|
|
||||||
features = ["env-filter", "tracing-log"]
|
|
||||||
|
|
||||||
[workspace.dependencies.urlencoding]
|
|
||||||
version = "2"
|
|
||||||
|
|
||||||
[workspace.dependencies.uuid]
|
|
||||||
version = "1.18.1"
|
|
||||||
features = ["js", "serde", "v1", "v3", "v4"]
|
|
||||||
|
|||||||
+35
-19
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lldap_app"
|
name = "lldap_app"
|
||||||
version = "0.6.3"
|
version = "0.6.2"
|
||||||
description = "Frontend for LLDAP"
|
description = "Frontend for LLDAP"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
include = ["src/**/*", "queries/**/*", "Cargo.toml", "../schema.graphql"]
|
include = ["src/**/*", "queries/**/*", "Cargo.toml", "../schema.graphql"]
|
||||||
@@ -11,34 +11,27 @@ repository.workspace = true
|
|||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { workspace = true }
|
anyhow = "1"
|
||||||
chrono = { workspace = true }
|
base64 = "0.13"
|
||||||
derive_more = { workspace = true }
|
gloo-console = "0.2.3"
|
||||||
gloo-console = "0.4"
|
gloo-file = "0.2.3"
|
||||||
gloo-file = "0.4"
|
gloo-net = "*"
|
||||||
gloo-net = "0.7"
|
graphql_client = "0.10"
|
||||||
graphql_client = { workspace = true }
|
http = "0.2"
|
||||||
image = { workspace = true }
|
jwt = "0.13"
|
||||||
jwt = { workspace = true }
|
rand = "0.8"
|
||||||
rand = { workspace = true }
|
serde_json = "1"
|
||||||
serde = { workspace = true }
|
|
||||||
serde_json = { workspace = true }
|
|
||||||
strum = { workspace = true }
|
|
||||||
percent-encoding = "2"
|
|
||||||
url-escape = "0.1.1"
|
url-escape = "0.1.1"
|
||||||
validator = "0.14"
|
validator = "0.14"
|
||||||
validator_derive = "0.14"
|
validator_derive = "0.14"
|
||||||
wasm-bindgen = "0.2.100"
|
wasm-bindgen = "0.2.100"
|
||||||
wasm-bindgen-futures = "0"
|
wasm-bindgen-futures = "*"
|
||||||
yew = "0.19.3"
|
yew = "0.19.3"
|
||||||
yew-router = "0.16"
|
yew-router = "0.16"
|
||||||
|
|
||||||
# Needed because of https://github.com/tkaitchuck/aHash/issues/95
|
# Needed because of https://github.com/tkaitchuck/aHash/issues/95
|
||||||
indexmap = "=1.6.2"
|
indexmap = "=1.6.2"
|
||||||
|
|
||||||
base64 = { workspace = true }
|
|
||||||
|
|
||||||
|
|
||||||
[dependencies.web-sys]
|
[dependencies.web-sys]
|
||||||
version = "0.3"
|
version = "0.3"
|
||||||
features = [
|
features = [
|
||||||
@@ -57,6 +50,17 @@ features = [
|
|||||||
"console",
|
"console",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[dependencies.chrono]
|
||||||
|
version = "*"
|
||||||
|
features = [
|
||||||
|
"wasmbind"
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependencies.derive_more]
|
||||||
|
features = ["debug", "display", "from", "from_str"]
|
||||||
|
default-features = false
|
||||||
|
version = "1"
|
||||||
|
|
||||||
[dependencies.lldap_auth]
|
[dependencies.lldap_auth]
|
||||||
path = "../crates/auth"
|
path = "../crates/auth"
|
||||||
features = [ "opaque_client" ]
|
features = [ "opaque_client" ]
|
||||||
@@ -67,6 +71,18 @@ path = "../crates/frontend-options"
|
|||||||
[dependencies.lldap_validation]
|
[dependencies.lldap_validation]
|
||||||
path = "../crates/validation"
|
path = "../crates/validation"
|
||||||
|
|
||||||
|
[dependencies.image]
|
||||||
|
features = ["jpeg"]
|
||||||
|
default-features = false
|
||||||
|
version = "0.24"
|
||||||
|
|
||||||
|
[dependencies.serde]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies.strum]
|
||||||
|
features = ["derive"]
|
||||||
|
version = "0.25"
|
||||||
|
|
||||||
[dependencies.yew_form]
|
[dependencies.yew_form]
|
||||||
git = "https://github.com/jfbilodeau/yew_form"
|
git = "https://github.com/jfbilodeau/yew_form"
|
||||||
rev = "4b9fabffb63393ec7626a4477fd36de12a07fac9"
|
rev = "4b9fabffb63393ec7626a4477fd36de12a07fac9"
|
||||||
|
|||||||
@@ -183,16 +183,6 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Percent-decode a URL path segment into a user ID string.
|
|
||||||
/// Returns `None` if the decoded bytes are not valid UTF-8, so the caller
|
|
||||||
/// can redirect to a safe page rather than silently mangling the ID.
|
|
||||||
fn decode_user_id(raw: &str) -> Option<String> {
|
|
||||||
percent_encoding::percent_decode_str(raw)
|
|
||||||
.decode_utf8()
|
|
||||||
.ok()
|
|
||||||
.map(|s| s.into_owned())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dispatch_route(
|
fn dispatch_route(
|
||||||
switch: &AppRoute,
|
switch: &AppRoute,
|
||||||
link: &Scope<Self>,
|
link: &Scope<Self>,
|
||||||
@@ -258,17 +248,11 @@ impl App {
|
|||||||
AppRoute::GroupDetails { group_id } => html! {
|
AppRoute::GroupDetails { group_id } => html! {
|
||||||
<GroupDetails group_id={*group_id} is_admin={is_admin} />
|
<GroupDetails group_id={*group_id} is_admin={is_admin} />
|
||||||
},
|
},
|
||||||
AppRoute::UserDetails { user_id } => match Self::decode_user_id(user_id) {
|
AppRoute::UserDetails { user_id } => html! {
|
||||||
Some(decoded_id) => html! {
|
<UserDetails username={user_id.clone()} is_admin={is_admin} />
|
||||||
<UserDetails username={decoded_id} is_admin={is_admin} />
|
|
||||||
},
|
|
||||||
None => html! { <Redirect to={AppRoute::Login} /> },
|
|
||||||
},
|
},
|
||||||
AppRoute::ChangePassword { user_id } => match Self::decode_user_id(user_id) {
|
AppRoute::ChangePassword { user_id } => html! {
|
||||||
Some(decoded_id) => html! {
|
<ChangePasswordForm username={user_id.clone()} is_admin={is_admin} />
|
||||||
<ChangePasswordForm username={decoded_id} is_admin={is_admin} />
|
|
||||||
},
|
|
||||||
None => html! { <Redirect to={AppRoute::Login} /> },
|
|
||||||
},
|
},
|
||||||
AppRoute::StartResetPassword => match password_reset_enabled {
|
AppRoute::StartResetPassword => match password_reset_enabled {
|
||||||
Some(true) => html! { <ResetPasswordStep1Form /> },
|
Some(true) => html! { <ResetPasswordStep1Form /> },
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ impl CommonComponent<CreateGroupForm> for CreateGroupForm {
|
|||||||
let model = self.form.model();
|
let model = self.form.model();
|
||||||
let req = create_group::Variables {
|
let req = create_group::Variables {
|
||||||
group: create_group::CreateGroupInput {
|
group: create_group::CreateGroupInput {
|
||||||
display_name: model.groupname,
|
displayName: model.groupname,
|
||||||
attributes,
|
attributes,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -143,9 +143,9 @@ impl CommonComponent<CreateUserForm> for CreateUserForm {
|
|||||||
user: create_user::CreateUserInput {
|
user: create_user::CreateUserInput {
|
||||||
id: model.username,
|
id: model.username,
|
||||||
email: None,
|
email: None,
|
||||||
display_name: None,
|
displayName: None,
|
||||||
first_name: None,
|
firstName: None,
|
||||||
last_name: None,
|
lastName: None,
|
||||||
avatar: None,
|
avatar: None,
|
||||||
attributes,
|
attributes,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use std::{fmt::Display, str::FromStr};
|
use std::{fmt::Display, str::FromStr};
|
||||||
|
|
||||||
use anyhow::{Error, Ok, Result, bail};
|
use anyhow::{Error, Ok, Result, bail};
|
||||||
use base64::Engine;
|
|
||||||
use gloo_file::{
|
use gloo_file::{
|
||||||
File,
|
File,
|
||||||
callbacks::{FileReader, read_as_bytes},
|
callbacks::{FileReader, read_as_bytes},
|
||||||
@@ -55,12 +54,12 @@ fn to_base64(file: &JsFile) -> Result<String> {
|
|||||||
if !is_valid_jpeg(data.as_slice()) {
|
if !is_valid_jpeg(data.as_slice()) {
|
||||||
bail!("Chosen image is not a valid JPEG");
|
bail!("Chosen image is not a valid JPEG");
|
||||||
}
|
}
|
||||||
Ok(base64::engine::general_purpose::STANDARD.encode(data))
|
Ok(base64::encode(data))
|
||||||
}
|
}
|
||||||
JsFile {
|
JsFile {
|
||||||
file: None,
|
file: None,
|
||||||
contents: Some(data),
|
contents: Some(data),
|
||||||
} => Ok(base64::engine::general_purpose::STANDARD.encode(data)),
|
} => Ok(base64::encode(data)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +98,7 @@ impl Component for JpegFileInput {
|
|||||||
.props()
|
.props()
|
||||||
.value
|
.value
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|x| base64::engine::general_purpose::STANDARD.decode(x).ok()),
|
.and_then(|x| base64::decode(x).ok()),
|
||||||
}),
|
}),
|
||||||
reader: None,
|
reader: None,
|
||||||
}
|
}
|
||||||
@@ -112,7 +111,7 @@ impl Component for JpegFileInput {
|
|||||||
.props()
|
.props()
|
||||||
.value
|
.value
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|x| base64::engine::general_purpose::STANDARD.decode(x).ok()),
|
.and_then(|x| base64::decode(x).ok()),
|
||||||
});
|
});
|
||||||
self.reader = None;
|
self.reader = None;
|
||||||
true
|
true
|
||||||
@@ -231,7 +230,7 @@ impl JpegFileInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn is_valid_jpeg(bytes: &[u8]) -> bool {
|
fn is_valid_jpeg(bytes: &[u8]) -> bool {
|
||||||
image::ImageReader::with_format(std::io::Cursor::new(bytes), image::ImageFormat::Jpeg)
|
image::io::Reader::with_format(std::io::Cursor::new(bytes), image::ImageFormat::Jpeg)
|
||||||
.decode()
|
.decode()
|
||||||
.is_ok()
|
.is_ok()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -248,13 +248,13 @@ impl GroupDetailsForm {
|
|||||||
};
|
};
|
||||||
let mut group_input = update_group::UpdateGroupInput {
|
let mut group_input = update_group::UpdateGroupInput {
|
||||||
id: self.group.id,
|
id: self.group.id,
|
||||||
display_name: None,
|
displayName: None,
|
||||||
remove_attributes: None,
|
removeAttributes: None,
|
||||||
insert_attributes: None,
|
insertAttributes: None,
|
||||||
};
|
};
|
||||||
let default_group_input = group_input.clone();
|
let default_group_input = group_input.clone();
|
||||||
group_input.remove_attributes = remove_attributes;
|
group_input.removeAttributes = remove_attributes;
|
||||||
group_input.insert_attributes = insert_attributes;
|
group_input.insertAttributes = insert_attributes;
|
||||||
// Nothing changed.
|
// Nothing changed.
|
||||||
if group_input == default_group_input {
|
if group_input == default_group_input {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
|
|||||||
@@ -260,16 +260,16 @@ impl UserDetailsForm {
|
|||||||
let mut user_input = update_user::UpdateUserInput {
|
let mut user_input = update_user::UpdateUserInput {
|
||||||
id: self.user.id.clone(),
|
id: self.user.id.clone(),
|
||||||
email: None,
|
email: None,
|
||||||
display_name: None,
|
displayName: None,
|
||||||
first_name: None,
|
firstName: None,
|
||||||
last_name: None,
|
lastName: None,
|
||||||
avatar: None,
|
avatar: None,
|
||||||
remove_attributes: None,
|
removeAttributes: None,
|
||||||
insert_attributes: None,
|
insertAttributes: None,
|
||||||
};
|
};
|
||||||
let default_user_input = user_input.clone();
|
let default_user_input = user_input.clone();
|
||||||
user_input.remove_attributes = remove_attributes;
|
user_input.removeAttributes = remove_attributes;
|
||||||
user_input.insert_attributes = insert_attributes;
|
user_input.insertAttributes = insert_attributes;
|
||||||
// Nothing changed.
|
// Nothing changed.
|
||||||
if user_input == default_user_input {
|
if user_input == default_user_input {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ impl HostService {
|
|||||||
where
|
where
|
||||||
QueryType: GraphQLQuery + 'static,
|
QueryType: GraphQLQuery + 'static,
|
||||||
{
|
{
|
||||||
let unwrap_graphql_response = |graphql_client::Response { data, errors, .. }| {
|
let unwrap_graphql_response = |graphql_client::Response { data, errors }| {
|
||||||
data.ok_or_else(|| {
|
data.ok_or_else(|| {
|
||||||
anyhow!(
|
anyhow!(
|
||||||
"Errors: [{}]",
|
"Errors: [{}]",
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
pub type DateTime = chrono::DateTime<chrono::Utc>;
|
pub type DateTimeUtc = chrono::DateTime<chrono::Utc>;
|
||||||
pub type DateTimeUtc = DateTime;
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lldap_access_control"
|
name = "lldap_access_control"
|
||||||
version = "0.1.1"
|
version = "0.1.0"
|
||||||
description = "Access control wrappers for LLDAP"
|
description = "Access control wrappers for LLDAP"
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
@@ -10,8 +10,8 @@ repository.workspace = true
|
|||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tracing = { workspace = true }
|
tracing = "*"
|
||||||
async-trait = { workspace = true }
|
async-trait = "0.1"
|
||||||
|
|
||||||
[dependencies.lldap_auth]
|
[dependencies.lldap_auth]
|
||||||
path = "../auth"
|
path = "../auth"
|
||||||
|
|||||||
+20
-8
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lldap_auth"
|
name = "lldap_auth"
|
||||||
version = "0.6.3"
|
version = "0.6.0"
|
||||||
description = "Authentication protocol for LLDAP"
|
description = "Authentication protocol for LLDAP"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
@@ -18,23 +18,35 @@ sea_orm = ["dep:sea-orm"]
|
|||||||
test = []
|
test = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chrono = { workspace = true }
|
|
||||||
derive_more = { workspace = true }
|
|
||||||
opaque-ke = { workspace = true }
|
|
||||||
serde = { workspace = true }
|
|
||||||
rust-argon2 = "2"
|
rust-argon2 = "2"
|
||||||
curve25519-dalek = "3"
|
curve25519-dalek = "3"
|
||||||
digest = "0.9"
|
digest = "0.9"
|
||||||
generic-array = "0.14"
|
generic-array = "0.14"
|
||||||
rand = { workspace = true }
|
rand = "0.8"
|
||||||
sha2 = "0.9"
|
sha2 = "0.9"
|
||||||
thiserror = { workspace = true }
|
thiserror = "2"
|
||||||
uuid = { workspace = true, features = ["serde"] }
|
uuid = { version = "1.18.1", features = ["serde"] }
|
||||||
|
|
||||||
|
[dependencies.derive_more]
|
||||||
|
features = ["debug", "display"]
|
||||||
|
default-features = false
|
||||||
|
version = "1"
|
||||||
|
|
||||||
|
[dependencies.opaque-ke]
|
||||||
|
version = "0.7"
|
||||||
|
|
||||||
|
[dependencies.chrono]
|
||||||
|
version = "*"
|
||||||
|
features = ["serde"]
|
||||||
|
|
||||||
[dependencies.sea-orm]
|
[dependencies.sea-orm]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
features = ["macros"]
|
||||||
optional = true
|
optional = true
|
||||||
|
|
||||||
|
[dependencies.serde]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
# For WASM targets, use the JS getrandom.
|
# For WASM targets, use the JS getrandom.
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.getrandom]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.getrandom]
|
||||||
version = "0.2"
|
version = "0.2"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lldap_domain_handlers"
|
name = "lldap_domain_handlers"
|
||||||
version = "0.1.1"
|
version = "0.1.0"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
homepage.workspace = true
|
homepage.workspace = true
|
||||||
@@ -12,17 +12,22 @@ rust-version.workspace = true
|
|||||||
test = []
|
test = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait = { workspace = true }
|
async-trait = "0.1"
|
||||||
base64 = { workspace = true }
|
base64 = "0.21"
|
||||||
chrono = { workspace = true }
|
ldap3_proto = "0.6.0"
|
||||||
derive_more = { workspace = true }
|
serde_bytes = "0.11"
|
||||||
ldap3_proto = { workspace = true }
|
|
||||||
serde = { workspace = true }
|
|
||||||
serde_bytes = { workspace = true }
|
|
||||||
uuid = { workspace = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pretty_assertions = { workspace = true }
|
pretty_assertions = "1"
|
||||||
|
|
||||||
|
[dependencies.chrono]
|
||||||
|
features = ["serde"]
|
||||||
|
version = "0.4"
|
||||||
|
|
||||||
|
[dependencies.derive_more]
|
||||||
|
features = ["debug", "display", "from", "from_str"]
|
||||||
|
default-features = false
|
||||||
|
version = "1"
|
||||||
|
|
||||||
[dependencies.lldap_auth]
|
[dependencies.lldap_auth]
|
||||||
path = "../auth"
|
path = "../auth"
|
||||||
@@ -33,3 +38,10 @@ path = "../domain"
|
|||||||
|
|
||||||
[dependencies.lldap_domain_model]
|
[dependencies.lldap_domain_model]
|
||||||
path = "../domain-model"
|
path = "../domain-model"
|
||||||
|
|
||||||
|
[dependencies.serde]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies.uuid]
|
||||||
|
features = ["v1", "v3"]
|
||||||
|
version = "1"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lldap_domain_model"
|
name = "lldap_domain_model"
|
||||||
version = "0.1.1"
|
version = "0.1.0"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
homepage.workspace = true
|
homepage.workspace = true
|
||||||
@@ -12,19 +12,23 @@ rust-version.workspace = true
|
|||||||
test = []
|
test = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
base64 = { workspace = true }
|
base64 = "0.21"
|
||||||
bincode = { workspace = true }
|
bincode = "1.3"
|
||||||
chrono = { workspace = true }
|
orion = "0.17"
|
||||||
derive_more = { workspace = true }
|
serde_bytes = "0.11"
|
||||||
orion = { workspace = true }
|
thiserror = "2"
|
||||||
sea-orm = { workspace = true }
|
|
||||||
serde = { workspace = true }
|
|
||||||
serde_bytes = { workspace = true }
|
|
||||||
thiserror = { workspace = true }
|
|
||||||
uuid = { workspace = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pretty_assertions = { workspace = true }
|
pretty_assertions = "1"
|
||||||
|
|
||||||
|
[dependencies.chrono]
|
||||||
|
features = ["serde"]
|
||||||
|
version = "0.4"
|
||||||
|
|
||||||
|
[dependencies.derive_more]
|
||||||
|
features = ["debug", "display", "from", "from_str"]
|
||||||
|
default-features = false
|
||||||
|
version = "1"
|
||||||
|
|
||||||
[dependencies.lldap_auth]
|
[dependencies.lldap_auth]
|
||||||
path = "../auth"
|
path = "../auth"
|
||||||
@@ -32,3 +36,14 @@ features = ["opaque_server", "opaque_client", "sea_orm"]
|
|||||||
|
|
||||||
[dependencies.lldap_domain]
|
[dependencies.lldap_domain]
|
||||||
path = "../domain"
|
path = "../domain"
|
||||||
|
|
||||||
|
[dependencies.sea-orm]
|
||||||
|
workspace = true
|
||||||
|
features = ["macros"]
|
||||||
|
|
||||||
|
[dependencies.serde]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies.uuid]
|
||||||
|
features = ["v1", "v3"]
|
||||||
|
version = "1"
|
||||||
|
|||||||
+44
-16
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lldap_domain"
|
name = "lldap_domain"
|
||||||
version = "0.1.1"
|
version = "0.1.0"
|
||||||
authors = [
|
authors = [
|
||||||
"Valentin Tolmer <valentin@tolmer.fr>",
|
"Valentin Tolmer <valentin@tolmer.fr>",
|
||||||
"Simon Broeng Jensen <sbj@cwconsult.dk>",
|
"Simon Broeng Jensen <sbj@cwconsult.dk>",
|
||||||
@@ -15,23 +15,51 @@ rust-version.workspace = true
|
|||||||
test = []
|
test = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { workspace = true }
|
anyhow = "*"
|
||||||
base64 = { workspace = true }
|
base64 = "0.21"
|
||||||
bincode = { workspace = true }
|
bincode = "1.3"
|
||||||
chrono = { workspace = true }
|
itertools = "0.10"
|
||||||
derive_more = { workspace = true }
|
juniper = "0.15"
|
||||||
image = { workspace = true }
|
serde_bytes = "0.11"
|
||||||
itertools = { workspace = true }
|
|
||||||
juniper = { workspace = true }
|
[dev-dependencies]
|
||||||
sea-orm = { workspace = true }
|
pretty_assertions = "1"
|
||||||
serde = { workspace = true }
|
|
||||||
serde_bytes = { workspace = true }
|
[dependencies.chrono]
|
||||||
strum = { workspace = true }
|
features = ["serde"]
|
||||||
uuid = { workspace = true }
|
version = "*"
|
||||||
|
|
||||||
|
[dependencies.derive_more]
|
||||||
|
features = ["debug", "display", "from", "from_str"]
|
||||||
|
default-features = false
|
||||||
|
version = "1"
|
||||||
|
|
||||||
|
[dependencies.image]
|
||||||
|
features = ["jpeg"]
|
||||||
|
default-features = false
|
||||||
|
version = "0.24"
|
||||||
|
|
||||||
[dependencies.lldap_auth]
|
[dependencies.lldap_auth]
|
||||||
path = "../auth"
|
path = "../auth"
|
||||||
features = ["opaque_server", "opaque_client", "sea_orm"]
|
features = ["opaque_server", "opaque_client", "sea_orm"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dependencies.sea-orm]
|
||||||
pretty_assertions = { workspace = true }
|
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"
|
||||||
|
|||||||
@@ -333,7 +333,7 @@ impl TryFrom<&[u8]> for JpegPhoto {
|
|||||||
return Ok(JpegPhoto::null());
|
return Ok(JpegPhoto::null());
|
||||||
}
|
}
|
||||||
// Confirm that it's a valid Jpeg, then store only the bytes.
|
// Confirm that it's a valid Jpeg, then store only the bytes.
|
||||||
image::ImageReader::with_format(std::io::Cursor::new(bytes), image::ImageFormat::Jpeg)
|
image::io::Reader::with_format(std::io::Cursor::new(bytes), image::ImageFormat::Jpeg)
|
||||||
.decode()?;
|
.decode()?;
|
||||||
Ok(JpegPhoto(bytes.to_vec()))
|
Ok(JpegPhoto(bytes.to_vec()))
|
||||||
}
|
}
|
||||||
@@ -346,7 +346,7 @@ impl TryFrom<Vec<u8>> for JpegPhoto {
|
|||||||
return Ok(JpegPhoto::null());
|
return Ok(JpegPhoto::null());
|
||||||
}
|
}
|
||||||
// Confirm that it's a valid Jpeg, then store only the bytes.
|
// Confirm that it's a valid Jpeg, then store only the bytes.
|
||||||
image::ImageReader::with_format(
|
image::io::Reader::with_format(
|
||||||
std::io::Cursor::new(bytes.as_slice()),
|
std::io::Cursor::new(bytes.as_slice()),
|
||||||
image::ImageFormat::Jpeg,
|
image::ImageFormat::Jpeg,
|
||||||
)
|
)
|
||||||
@@ -397,7 +397,7 @@ impl JpegPhoto {
|
|||||||
|
|
||||||
#[cfg(any(feature = "test", test))]
|
#[cfg(any(feature = "test", test))]
|
||||||
pub fn for_tests() -> Self {
|
pub fn for_tests() -> Self {
|
||||||
use image::{ImageFormat, Rgb, RgbImage};
|
use image::{ImageOutputFormat, Rgb, RgbImage};
|
||||||
let img = RgbImage::from_fn(32, 32, |x, y| {
|
let img = RgbImage::from_fn(32, 32, |x, y| {
|
||||||
if (x + y) % 2 == 0 {
|
if (x + y) % 2 == 0 {
|
||||||
Rgb([0, 0, 0])
|
Rgb([0, 0, 0])
|
||||||
@@ -406,8 +406,11 @@ impl JpegPhoto {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
let mut bytes: Vec<u8> = Vec::new();
|
let mut bytes: Vec<u8> = Vec::new();
|
||||||
img.write_to(&mut std::io::Cursor::new(&mut bytes), ImageFormat::Jpeg)
|
img.write_to(
|
||||||
.unwrap();
|
&mut std::io::Cursor::new(&mut bytes),
|
||||||
|
ImageOutputFormat::Jpeg(0),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
Self(bytes)
|
Self(bytes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -685,7 +688,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&format!("{:?}", Serialized::from(&JpegPhoto::for_tests())),
|
&format!("{:?}", Serialized::from(&JpegPhoto::for_tests())),
|
||||||
"Serialized(\"hash: 0xBB3017828B2F3DEF\")"
|
"Serialized(\"hash: 0xB947C77A16F3C3BD\")"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lldap_frontend_options"
|
name = "lldap_frontend_options"
|
||||||
version = "0.1.1"
|
version = "0.1.0"
|
||||||
description = "Frontend options for LLDAP"
|
description = "Frontend options for LLDAP"
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
@@ -9,5 +9,5 @@ license.workspace = true
|
|||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies.serde]
|
||||||
serde = { workspace = true }
|
workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lldap_graphql_server"
|
name = "lldap_graphql_server"
|
||||||
version = "0.1.1"
|
version = "0.1.0"
|
||||||
description = "GraphQL server for LLDAP"
|
description = "GraphQL server for LLDAP"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
@@ -10,14 +10,15 @@ repository.workspace = true
|
|||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { workspace = true }
|
anyhow = "*"
|
||||||
chrono = { workspace = true }
|
juniper = "0.15"
|
||||||
juniper = { workspace = true }
|
serde_json = "1"
|
||||||
serde = { workspace = true }
|
tracing = "*"
|
||||||
serde_json = { workspace = true }
|
urlencoding = "2"
|
||||||
tracing = { workspace = true }
|
|
||||||
urlencoding = { workspace = true }
|
[dependencies.chrono]
|
||||||
uuid = { workspace = true }
|
features = ["serde"]
|
||||||
|
version = "*"
|
||||||
|
|
||||||
[dependencies.lldap_access_control]
|
[dependencies.lldap_access_control]
|
||||||
path = "../access-control"
|
path = "../access-control"
|
||||||
@@ -44,10 +45,16 @@ path = "../sql-backend-handler"
|
|||||||
[dependencies.lldap_validation]
|
[dependencies.lldap_validation]
|
||||||
path = "../validation"
|
path = "../validation"
|
||||||
|
|
||||||
|
[dependencies.serde]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies.uuid]
|
||||||
|
features = ["v1", "v3"]
|
||||||
|
version = "1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
mockall = { workspace = true }
|
mockall = "0.11.4"
|
||||||
pretty_assertions = { workspace = true }
|
pretty_assertions = "1"
|
||||||
tokio = { workspace = true }
|
|
||||||
|
|
||||||
#[dev-dependencies.lldap_auth]
|
#[dev-dependencies.lldap_auth]
|
||||||
#path = "../auth"
|
#path = "../auth"
|
||||||
@@ -63,3 +70,7 @@ path = "../test-utils"
|
|||||||
#[dev-dependencies.lldap_sql_backend_handler]
|
#[dev-dependencies.lldap_sql_backend_handler]
|
||||||
#path = "../sql-backend-handler"
|
#path = "../sql-backend-handler"
|
||||||
#features = ["test"]
|
#features = ["test"]
|
||||||
|
|
||||||
|
[dev-dependencies.tokio]
|
||||||
|
features = ["full"]
|
||||||
|
version = "1.25"
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ impl<Handler: BackendHandler> Context<Handler> {
|
|||||||
impl<Handler: BackendHandler> juniper::Context for Context<Handler> {}
|
impl<Handler: BackendHandler> juniper::Context for Context<Handler> {}
|
||||||
|
|
||||||
type Schema<Handler> =
|
type Schema<Handler> =
|
||||||
RootNode<Query<Handler>, Mutation<Handler>, EmptySubscription<Context<Handler>>>;
|
RootNode<'static, Query<Handler>, Mutation<Handler>, EmptySubscription<Context<Handler>>>;
|
||||||
|
|
||||||
pub fn schema<Handler: BackendHandler>() -> Schema<Handler> {
|
pub fn schema<Handler: BackendHandler>() -> Schema<Handler> {
|
||||||
Schema::new(
|
Schema::new(
|
||||||
@@ -73,7 +73,7 @@ pub fn schema<Handler: BackendHandler>() -> Schema<Handler> {
|
|||||||
pub fn export_schema(output_file: Option<String>) -> anyhow::Result<()> {
|
pub fn export_schema(output_file: Option<String>) -> anyhow::Result<()> {
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use lldap_sql_backend_handler::SqlBackendHandler;
|
use lldap_sql_backend_handler::SqlBackendHandler;
|
||||||
let output = schema::<SqlBackendHandler>().as_sdl();
|
let output = schema::<SqlBackendHandler>().as_schema_language();
|
||||||
match output_file {
|
match output_file {
|
||||||
None => println!("{output}"),
|
None => println!("{output}"),
|
||||||
Some(path) => {
|
Some(path) => {
|
||||||
|
|||||||
@@ -561,13 +561,13 @@ mod tests {
|
|||||||
use mockall::predicate::eq;
|
use mockall::predicate::eq;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
fn mutation_schema<C, Q, M>(
|
fn mutation_schema<'q, C, Q, M>(
|
||||||
query_root: Q,
|
query_root: Q,
|
||||||
mutation_root: M,
|
mutation_root: M,
|
||||||
) -> RootNode<Q, M, EmptySubscription<C>>
|
) -> RootNode<'q, Q, M, EmptySubscription<C>>
|
||||||
where
|
where
|
||||||
Q: GraphQLType<DefaultScalarValue, Context = C, TypeInfo = ()>,
|
Q: GraphQLType<DefaultScalarValue, Context = C, TypeInfo = ()> + 'q,
|
||||||
M: GraphQLType<DefaultScalarValue, Context = C, TypeInfo = ()>,
|
M: GraphQLType<DefaultScalarValue, Context = C, TypeInfo = ()> + 'q,
|
||||||
{
|
{
|
||||||
RootNode::new(query_root, mutation_root, EmptySubscription::<C>::new())
|
RootNode::new(query_root, mutation_root, EmptySubscription::<C>::new())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ impl<Handler: BackendHandler> AttributeValue<Handler> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn attribute_name(&self) -> &str {
|
pub(super) fn name(&self) -> &str {
|
||||||
self.attribute.name.as_str()
|
self.attribute.name.as_str()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,11 +47,7 @@ impl<Handler: BackendHandler> Query<Handler> {
|
|||||||
"1.0"
|
"1.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn user(
|
pub async fn user(context: &Context<Handler>, user_id: String) -> FieldResult<User<Handler>> {
|
||||||
&self,
|
|
||||||
context: &Context<Handler>,
|
|
||||||
user_id: String,
|
|
||||||
) -> FieldResult<User<Handler>> {
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
let span = debug_span!("[GraphQL query] user");
|
let span = debug_span!("[GraphQL query] user");
|
||||||
span.in_scope(|| {
|
span.in_scope(|| {
|
||||||
@@ -65,15 +61,14 @@ impl<Handler: BackendHandler> Query<Handler> {
|
|||||||
&span,
|
&span,
|
||||||
"Unauthorized access to user data",
|
"Unauthorized access to user data",
|
||||||
))?;
|
))?;
|
||||||
let schema: Arc<PublicSchema> = Arc::new(self.get_schema(context, span.clone()).await?);
|
let schema = Arc::new(self.get_schema(context, span.clone()).await?);
|
||||||
let user = handler.get_user_details(&user_id).instrument(span).await?;
|
let user = handler.get_user_details(&user_id).instrument(span).await?;
|
||||||
User::<Handler>::from_user(user, schema)
|
User::<Handler>::from_user(user, schema)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn users(
|
async fn users(
|
||||||
&self,
|
|
||||||
context: &Context<Handler>,
|
context: &Context<Handler>,
|
||||||
filters: Option<RequestFilter>,
|
#[graphql(name = "where")] filters: Option<RequestFilter>,
|
||||||
) -> FieldResult<Vec<User<Handler>>> {
|
) -> FieldResult<Vec<User<Handler>>> {
|
||||||
let span = debug_span!("[GraphQL query] users");
|
let span = debug_span!("[GraphQL query] users");
|
||||||
span.in_scope(|| {
|
span.in_scope(|| {
|
||||||
@@ -85,7 +80,7 @@ impl<Handler: BackendHandler> Query<Handler> {
|
|||||||
&span,
|
&span,
|
||||||
"Unauthorized access to user list",
|
"Unauthorized access to user list",
|
||||||
))?;
|
))?;
|
||||||
let schema: Arc<PublicSchema> = Arc::new(self.get_schema(context, span.clone()).await?);
|
let schema = Arc::new(self.get_schema(context, span.clone()).await?);
|
||||||
let users = handler
|
let users = handler
|
||||||
.list_users(
|
.list_users(
|
||||||
filters
|
filters
|
||||||
@@ -101,7 +96,7 @@ impl<Handler: BackendHandler> Query<Handler> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn groups(&self, context: &Context<Handler>) -> FieldResult<Vec<Group<Handler>>> {
|
async fn groups(context: &Context<Handler>) -> FieldResult<Vec<Group<Handler>>> {
|
||||||
let span = debug_span!("[GraphQL query] groups");
|
let span = debug_span!("[GraphQL query] groups");
|
||||||
let handler = context
|
let handler = context
|
||||||
.get_readonly_handler()
|
.get_readonly_handler()
|
||||||
@@ -109,7 +104,7 @@ impl<Handler: BackendHandler> Query<Handler> {
|
|||||||
&span,
|
&span,
|
||||||
"Unauthorized access to group list",
|
"Unauthorized access to group list",
|
||||||
))?;
|
))?;
|
||||||
let schema: Arc<PublicSchema> = Arc::new(self.get_schema(context, span.clone()).await?);
|
let schema = Arc::new(self.get_schema(context, span.clone()).await?);
|
||||||
let domain_groups = handler.list_groups(None).instrument(span).await?;
|
let domain_groups = handler.list_groups(None).instrument(span).await?;
|
||||||
domain_groups
|
domain_groups
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -117,11 +112,7 @@ impl<Handler: BackendHandler> Query<Handler> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn group(
|
async fn group(context: &Context<Handler>, group_id: i32) -> FieldResult<Group<Handler>> {
|
||||||
&self,
|
|
||||||
context: &Context<Handler>,
|
|
||||||
group_id: i32,
|
|
||||||
) -> FieldResult<Group<Handler>> {
|
|
||||||
let span = debug_span!("[GraphQL query] group");
|
let span = debug_span!("[GraphQL query] group");
|
||||||
span.in_scope(|| {
|
span.in_scope(|| {
|
||||||
debug!(?group_id);
|
debug!(?group_id);
|
||||||
@@ -132,7 +123,7 @@ impl<Handler: BackendHandler> Query<Handler> {
|
|||||||
&span,
|
&span,
|
||||||
"Unauthorized access to group data",
|
"Unauthorized access to group data",
|
||||||
))?;
|
))?;
|
||||||
let schema: Arc<PublicSchema> = Arc::new(self.get_schema(context, span.clone()).await?);
|
let schema = Arc::new(self.get_schema(context, span.clone()).await?);
|
||||||
let group_details = handler
|
let group_details = handler
|
||||||
.get_group_details(GroupId(group_id))
|
.get_group_details(GroupId(group_id))
|
||||||
.instrument(span)
|
.instrument(span)
|
||||||
@@ -140,7 +131,7 @@ impl<Handler: BackendHandler> Query<Handler> {
|
|||||||
Group::<Handler>::from_group_details(group_details, schema.clone())
|
Group::<Handler>::from_group_details(group_details, schema.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn schema(&self, context: &Context<Handler>) -> FieldResult<Schema<Handler>> {
|
async fn schema(context: &Context<Handler>) -> FieldResult<Schema<Handler>> {
|
||||||
let span = debug_span!("[GraphQL query] get_schema");
|
let span = debug_span!("[GraphQL query] get_schema");
|
||||||
self.get_schema(context, span).await.map(Into::into)
|
self.get_schema(context, span).await.map(Into::into)
|
||||||
}
|
}
|
||||||
@@ -184,9 +175,9 @@ mod tests {
|
|||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
fn schema<C, Q>(query_root: Q) -> RootNode<Q, EmptyMutation<C>, EmptySubscription<C>>
|
fn schema<'q, C, Q>(query_root: Q) -> RootNode<'q, Q, EmptyMutation<C>, EmptySubscription<C>>
|
||||||
where
|
where
|
||||||
Q: GraphQLType<DefaultScalarValue, Context = C, TypeInfo = ()>,
|
Q: GraphQLType<DefaultScalarValue, Context = C, TypeInfo = ()> + 'q,
|
||||||
{
|
{
|
||||||
RootNode::new(
|
RootNode::new(
|
||||||
query_root,
|
query_root,
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ impl<Handler: BackendHandler> User<Handler> {
|
|||||||
fn first_name(&self) -> &str {
|
fn first_name(&self) -> &str {
|
||||||
self.attributes
|
self.attributes
|
||||||
.iter()
|
.iter()
|
||||||
.find(|a| a.attribute_name() == "first_name")
|
.find(|a| a.name() == "first_name")
|
||||||
.map(|a| a.attribute.value.as_str().unwrap_or_default())
|
.map(|a| a.attribute.value.as_str().unwrap_or_default())
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
@@ -78,7 +78,7 @@ impl<Handler: BackendHandler> User<Handler> {
|
|||||||
fn last_name(&self) -> &str {
|
fn last_name(&self) -> &str {
|
||||||
self.attributes
|
self.attributes
|
||||||
.iter()
|
.iter()
|
||||||
.find(|a| a.attribute_name() == "last_name")
|
.find(|a| a.name() == "last_name")
|
||||||
.map(|a| a.attribute.value.as_str().unwrap_or_default())
|
.map(|a| a.attribute.value.as_str().unwrap_or_default())
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
@@ -86,7 +86,7 @@ impl<Handler: BackendHandler> User<Handler> {
|
|||||||
fn avatar(&self) -> Option<String> {
|
fn avatar(&self) -> Option<String> {
|
||||||
self.attributes
|
self.attributes
|
||||||
.iter()
|
.iter()
|
||||||
.find(|a| a.attribute_name() == "avatar")
|
.find(|a| a.name() == "avatar")
|
||||||
.map(|a| {
|
.map(|a| {
|
||||||
String::from(
|
String::from(
|
||||||
a.attribute
|
a.attribute
|
||||||
|
|||||||
+23
-13
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lldap_ldap"
|
name = "lldap_ldap"
|
||||||
version = "0.1.1"
|
version = "0.1.0"
|
||||||
description = "LDAP operations support"
|
description = "LDAP operations support"
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
@@ -10,19 +10,27 @@ repository.workspace = true
|
|||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { workspace = true }
|
anyhow = "*"
|
||||||
tracing = { workspace = true }
|
ldap3_proto = "0.6.0"
|
||||||
itertools = { workspace = true }
|
tracing = "*"
|
||||||
|
itertools = "0.10"
|
||||||
|
|
||||||
derive_more = { workspace = true }
|
[dependencies.derive_more]
|
||||||
|
features = ["from"]
|
||||||
|
default-features = false
|
||||||
|
version = "1"
|
||||||
|
|
||||||
chrono = { workspace = true }
|
[dependencies.chrono]
|
||||||
|
features = ["serde"]
|
||||||
|
version = "*"
|
||||||
|
|
||||||
rand = { workspace = true }
|
[dependencies.rand]
|
||||||
|
features = ["small_rng", "getrandom"]
|
||||||
|
version = "0.8"
|
||||||
|
|
||||||
uuid = { workspace = true }
|
[dependencies.uuid]
|
||||||
|
version = "1"
|
||||||
ldap3_proto = { workspace = true }
|
features = ["v1", "v3"]
|
||||||
|
|
||||||
[dependencies.lldap_access_control]
|
[dependencies.lldap_access_control]
|
||||||
path = "../access-control"
|
path = "../access-control"
|
||||||
@@ -47,10 +55,12 @@ path = "../opaque-handler"
|
|||||||
path = "../test-utils"
|
path = "../test-utils"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
mockall = { workspace = true }
|
mockall = "0.11.4"
|
||||||
pretty_assertions = { workspace = true }
|
pretty_assertions = "1"
|
||||||
|
|
||||||
tokio = { workspace = true }
|
[dev-dependencies.tokio]
|
||||||
|
features = ["full"]
|
||||||
|
version = "1.25"
|
||||||
|
|
||||||
[dev-dependencies.lldap_domain]
|
[dev-dependencies.lldap_domain]
|
||||||
path = "../domain"
|
path = "../domain"
|
||||||
|
|||||||
@@ -173,31 +173,22 @@ fn get_group_attribute_equality_filter(
|
|||||||
typ: AttributeType,
|
typ: AttributeType,
|
||||||
is_list: bool,
|
is_list: bool,
|
||||||
value: &str,
|
value: &str,
|
||||||
) -> LdapResult<GroupRequestFilter> {
|
) -> GroupRequestFilter {
|
||||||
if is_list {
|
|
||||||
return Err(LdapError {
|
|
||||||
code: LdapResultCode::UnwillingToPerform,
|
|
||||||
message: format!(
|
|
||||||
"Equality filter on list attribute \"{}\" is not supported",
|
|
||||||
field
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let value_lc = value.to_ascii_lowercase();
|
let value_lc = value.to_ascii_lowercase();
|
||||||
let serialized_value = deserialize_attribute_value(&[value.to_owned()], typ, false);
|
let serialized_value = deserialize_attribute_value(&[value.to_owned()], typ, is_list);
|
||||||
let serialized_value_lc = deserialize_attribute_value(&[value_lc.to_owned()], typ, false);
|
let serialized_value_lc = deserialize_attribute_value(&[value_lc.to_owned()], typ, is_list);
|
||||||
match (serialized_value, serialized_value_lc) {
|
match (serialized_value, serialized_value_lc) {
|
||||||
(Ok(v), Ok(v_lc)) => Ok(GroupRequestFilter::Or(vec![
|
(Ok(v), Ok(v_lc)) => GroupRequestFilter::Or(vec![
|
||||||
GroupRequestFilter::AttributeEquality(field.clone(), v),
|
GroupRequestFilter::AttributeEquality(field.clone(), v),
|
||||||
GroupRequestFilter::AttributeEquality(field.clone(), v_lc),
|
GroupRequestFilter::AttributeEquality(field.clone(), v_lc),
|
||||||
])),
|
]),
|
||||||
(Ok(_), Err(e)) => {
|
(Ok(_), Err(e)) => {
|
||||||
warn!("Invalid value for attribute {} (lowercased): {}", field, e);
|
warn!("Invalid value for attribute {} (lowercased): {}", field, e);
|
||||||
Ok(GroupRequestFilter::False)
|
GroupRequestFilter::False
|
||||||
}
|
}
|
||||||
(Err(e), _) => {
|
(Err(e), _) => {
|
||||||
warn!("Invalid value for attribute {}: {}", field, e);
|
warn!("Invalid value for attribute {}: {}", field, e);
|
||||||
Ok(GroupRequestFilter::False)
|
GroupRequestFilter::False
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -268,9 +259,9 @@ fn convert_group_filter(
|
|||||||
}
|
}
|
||||||
Ok(GroupRequestFilter::False)
|
Ok(GroupRequestFilter::False)
|
||||||
}
|
}
|
||||||
GroupFieldType::Attribute(field, typ, is_list) => {
|
GroupFieldType::Attribute(field, typ, is_list) => Ok(
|
||||||
get_group_attribute_equality_filter(&field, typ, is_list, value)
|
get_group_attribute_equality_filter(&field, typ, is_list, value),
|
||||||
}
|
),
|
||||||
GroupFieldType::CreationDate => Err(LdapError {
|
GroupFieldType::CreationDate => Err(LdapError {
|
||||||
code: LdapResultCode::UnwillingToPerform,
|
code: LdapResultCode::UnwillingToPerform,
|
||||||
message: "Creation date filter for groups not supported".to_owned(),
|
message: "Creation date filter for groups not supported".to_owned(),
|
||||||
@@ -680,82 +671,6 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_equality_filter_on_list_group_attribute_returns_error() {
|
|
||||||
use lldap_domain::schema::{AttributeList, AttributeSchema, Schema};
|
|
||||||
let mut mock = MockTestBackendHandler::new();
|
|
||||||
mock.expect_get_schema().returning(|| {
|
|
||||||
Ok(Schema {
|
|
||||||
user_attributes: AttributeList {
|
|
||||||
attributes: Vec::new(),
|
|
||||||
},
|
|
||||||
group_attributes: AttributeList {
|
|
||||||
attributes: vec![AttributeSchema {
|
|
||||||
name: "tags".into(),
|
|
||||||
attribute_type: AttributeType::String,
|
|
||||||
is_list: true,
|
|
||||||
is_visible: true,
|
|
||||||
is_editable: true,
|
|
||||||
is_hardcoded: false,
|
|
||||||
is_readonly: false,
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
extra_user_object_classes: Vec::new(),
|
|
||||||
extra_group_object_classes: Vec::new(),
|
|
||||||
})
|
|
||||||
});
|
|
||||||
let ldap_handler = setup_bound_admin_handler(mock).await;
|
|
||||||
let request = make_group_search_request(
|
|
||||||
LdapFilter::Equality("tags".to_string(), "foo".to_string()),
|
|
||||||
vec!["dn"],
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
ldap_handler.do_search_or_dse(&request).await,
|
|
||||||
Err(LdapError {
|
|
||||||
code: LdapResultCode::UnwillingToPerform,
|
|
||||||
message: r#"Equality filter on list attribute "tags" is not supported"#.to_string(),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_equality_filter_on_non_list_group_attribute() {
|
|
||||||
use lldap_domain::schema::{AttributeList, AttributeSchema, Schema};
|
|
||||||
let mut mock = MockTestBackendHandler::new();
|
|
||||||
mock.expect_get_schema().returning(|| {
|
|
||||||
Ok(Schema {
|
|
||||||
user_attributes: AttributeList {
|
|
||||||
attributes: Vec::new(),
|
|
||||||
},
|
|
||||||
group_attributes: AttributeList {
|
|
||||||
attributes: vec![AttributeSchema {
|
|
||||||
name: "club_name".into(),
|
|
||||||
attribute_type: AttributeType::String,
|
|
||||||
is_list: false,
|
|
||||||
is_visible: true,
|
|
||||||
is_editable: true,
|
|
||||||
is_hardcoded: false,
|
|
||||||
is_readonly: false,
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
extra_user_object_classes: Vec::new(),
|
|
||||||
extra_group_object_classes: Vec::new(),
|
|
||||||
})
|
|
||||||
});
|
|
||||||
mock.expect_list_groups()
|
|
||||||
.times(1)
|
|
||||||
.return_once(|_| Ok(vec![]));
|
|
||||||
let ldap_handler = setup_bound_admin_handler(mock).await;
|
|
||||||
let request = make_group_search_request(
|
|
||||||
LdapFilter::Equality("club_name".to_string(), "myclub".to_string()),
|
|
||||||
vec!["dn"],
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
ldap_handler.do_search_or_dse(&request).await,
|
|
||||||
Ok(vec![make_search_success()])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_search_group_as_scope() {
|
async fn test_search_group_as_scope() {
|
||||||
let mut mock = MockTestBackendHandler::new();
|
let mut mock = MockTestBackendHandler::new();
|
||||||
|
|||||||
@@ -182,31 +182,22 @@ fn get_user_attribute_equality_filter(
|
|||||||
typ: AttributeType,
|
typ: AttributeType,
|
||||||
is_list: bool,
|
is_list: bool,
|
||||||
value: &str,
|
value: &str,
|
||||||
) -> LdapResult<UserRequestFilter> {
|
) -> UserRequestFilter {
|
||||||
if is_list {
|
|
||||||
return Err(LdapError {
|
|
||||||
code: LdapResultCode::UnwillingToPerform,
|
|
||||||
message: format!(
|
|
||||||
"Equality filter on list attribute \"{}\" is not supported",
|
|
||||||
field
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let value_lc = value.to_ascii_lowercase();
|
let value_lc = value.to_ascii_lowercase();
|
||||||
let serialized_value = deserialize_attribute_value(&[value.to_owned()], typ, false);
|
let serialized_value = deserialize_attribute_value(&[value.to_owned()], typ, is_list);
|
||||||
let serialized_value_lc = deserialize_attribute_value(&[value_lc.to_owned()], typ, false);
|
let serialized_value_lc = deserialize_attribute_value(&[value_lc.to_owned()], typ, is_list);
|
||||||
match (serialized_value, serialized_value_lc) {
|
match (serialized_value, serialized_value_lc) {
|
||||||
(Ok(v), Ok(v_lc)) => Ok(UserRequestFilter::Or(vec![
|
(Ok(v), Ok(v_lc)) => UserRequestFilter::Or(vec![
|
||||||
UserRequestFilter::AttributeEquality(field.clone(), v),
|
UserRequestFilter::AttributeEquality(field.clone(), v),
|
||||||
UserRequestFilter::AttributeEquality(field.clone(), v_lc),
|
UserRequestFilter::AttributeEquality(field.clone(), v_lc),
|
||||||
])),
|
]),
|
||||||
(Ok(_), Err(e)) => {
|
(Ok(_), Err(e)) => {
|
||||||
warn!("Invalid value for attribute {} (lowercased): {}", field, e);
|
warn!("Invalid value for attribute {} (lowercased): {}", field, e);
|
||||||
Ok(UserRequestFilter::False)
|
UserRequestFilter::False
|
||||||
}
|
}
|
||||||
(Err(e), _) => {
|
(Err(e), _) => {
|
||||||
warn!("Invalid value for attribute {}: {}", field, e);
|
warn!("Invalid value for attribute {}: {}", field, e);
|
||||||
Ok(UserRequestFilter::False)
|
UserRequestFilter::False
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -288,9 +279,9 @@ fn convert_user_filter(
|
|||||||
UserFieldType::PrimaryField(field) => {
|
UserFieldType::PrimaryField(field) => {
|
||||||
Ok(UserRequestFilter::Equality(field, value_lc))
|
Ok(UserRequestFilter::Equality(field, value_lc))
|
||||||
}
|
}
|
||||||
UserFieldType::Attribute(field, typ, is_list) => {
|
UserFieldType::Attribute(field, typ, is_list) => Ok(
|
||||||
get_user_attribute_equality_filter(&field, typ, is_list, value)
|
get_user_attribute_equality_filter(&field, typ, is_list, value),
|
||||||
}
|
),
|
||||||
UserFieldType::NoMatch => {
|
UserFieldType::NoMatch => {
|
||||||
if !ldap_info.ignored_user_attributes.contains(&field) {
|
if !ldap_info.ignored_user_attributes.contains(&field) {
|
||||||
warn!(
|
warn!(
|
||||||
@@ -795,83 +786,6 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_equality_filter_on_list_user_attribute_returns_error() {
|
|
||||||
use lldap_domain::schema::{AttributeList, AttributeSchema, Schema};
|
|
||||||
let mut mock = MockTestBackendHandler::new();
|
|
||||||
mock.expect_get_schema().returning(|| {
|
|
||||||
Ok(Schema {
|
|
||||||
user_attributes: AttributeList {
|
|
||||||
attributes: vec![AttributeSchema {
|
|
||||||
name: "mailalias".into(),
|
|
||||||
attribute_type: AttributeType::String,
|
|
||||||
is_list: true,
|
|
||||||
is_visible: true,
|
|
||||||
is_editable: true,
|
|
||||||
is_hardcoded: false,
|
|
||||||
is_readonly: false,
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
group_attributes: AttributeList {
|
|
||||||
attributes: Vec::new(),
|
|
||||||
},
|
|
||||||
extra_user_object_classes: Vec::new(),
|
|
||||||
extra_group_object_classes: Vec::new(),
|
|
||||||
})
|
|
||||||
});
|
|
||||||
let ldap_handler = setup_bound_admin_handler(mock).await;
|
|
||||||
let request = make_user_search_request(
|
|
||||||
LdapFilter::Equality("mailalias".to_string(), "alias@example.com".to_string()),
|
|
||||||
vec!["dn"],
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
ldap_handler.do_search_or_dse(&request).await,
|
|
||||||
Err(LdapError {
|
|
||||||
code: LdapResultCode::UnwillingToPerform,
|
|
||||||
message: r#"Equality filter on list attribute "mailalias" is not supported"#
|
|
||||||
.to_string(),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_equality_filter_on_non_list_user_attribute() {
|
|
||||||
use lldap_domain::schema::{AttributeList, AttributeSchema, Schema};
|
|
||||||
let mut mock = MockTestBackendHandler::new();
|
|
||||||
mock.expect_get_schema().returning(|| {
|
|
||||||
Ok(Schema {
|
|
||||||
user_attributes: AttributeList {
|
|
||||||
attributes: vec![AttributeSchema {
|
|
||||||
name: "nickname".into(),
|
|
||||||
attribute_type: AttributeType::String,
|
|
||||||
is_list: false,
|
|
||||||
is_visible: true,
|
|
||||||
is_editable: true,
|
|
||||||
is_hardcoded: false,
|
|
||||||
is_readonly: false,
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
group_attributes: AttributeList {
|
|
||||||
attributes: Vec::new(),
|
|
||||||
},
|
|
||||||
extra_user_object_classes: Vec::new(),
|
|
||||||
extra_group_object_classes: Vec::new(),
|
|
||||||
})
|
|
||||||
});
|
|
||||||
mock.expect_list_users()
|
|
||||||
.times(1)
|
|
||||||
.return_once(|_, _| Ok(vec![]));
|
|
||||||
let ldap_handler = setup_bound_admin_handler(mock).await;
|
|
||||||
let request = make_user_search_request(
|
|
||||||
LdapFilter::Equality("nickname".to_string(), "alice".to_string()),
|
|
||||||
vec!["dn"],
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
ldap_handler.do_search_or_dse(&request).await,
|
|
||||||
Ok(vec![make_search_success()])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_search_cn_case_insensitive() {
|
async fn test_search_cn_case_insensitive() {
|
||||||
use lldap_domain::uuid;
|
use lldap_domain::uuid;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lldap_opaque_handler"
|
name = "lldap_opaque_handler"
|
||||||
version = "0.1.1"
|
version = "0.1.0"
|
||||||
description = "Opaque handler trait for LLDAP"
|
description = "Opaque handler trait for LLDAP"
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
@@ -13,7 +13,7 @@ rust-version.workspace = true
|
|||||||
test = []
|
test = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait = { workspace = true }
|
async-trait = "0.1"
|
||||||
|
|
||||||
[dependencies.lldap_auth]
|
[dependencies.lldap_auth]
|
||||||
path = "../auth"
|
path = "../auth"
|
||||||
@@ -26,4 +26,4 @@ path = "../domain"
|
|||||||
path = "../domain-model"
|
path = "../domain-model"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
mockall = { workspace = true }
|
mockall = "0.11.4"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lldap_sql_backend_handler"
|
name = "lldap_sql_backend_handler"
|
||||||
version = "0.1.1"
|
version = "0.1.0"
|
||||||
description = "SQL backend for LLDAP"
|
description = "SQL backend for LLDAP"
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
@@ -13,30 +13,44 @@ rust-version.workspace = true
|
|||||||
test = []
|
test = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { workspace = true }
|
anyhow = "*"
|
||||||
async-trait = { workspace = true }
|
async-trait = "0.1"
|
||||||
itertools = { workspace = true }
|
base64 = "0.21"
|
||||||
orion = { workspace = true }
|
bincode = "1.3"
|
||||||
serde_json = { workspace = true }
|
itertools = "0.10"
|
||||||
tracing = { workspace = true }
|
ldap3_proto = "0.6.0"
|
||||||
|
orion = "0.17"
|
||||||
|
serde_json = "1"
|
||||||
|
tracing = "*"
|
||||||
|
|
||||||
bincode = { workspace = true }
|
[dependencies.chrono]
|
||||||
|
features = ["serde"]
|
||||||
|
version = "*"
|
||||||
|
|
||||||
base64 = { workspace = true }
|
[dependencies.rand]
|
||||||
|
features = ["small_rng", "getrandom"]
|
||||||
|
version = "0.8"
|
||||||
|
|
||||||
chrono = { workspace = true }
|
[dependencies.sea-orm]
|
||||||
|
workspace = true
|
||||||
|
features = [
|
||||||
|
"macros",
|
||||||
|
"with-chrono",
|
||||||
|
"with-uuid",
|
||||||
|
"sqlx-all",
|
||||||
|
"runtime-actix-rustls",
|
||||||
|
]
|
||||||
|
|
||||||
rand = { workspace = true }
|
[dependencies.secstr]
|
||||||
|
features = ["serde"]
|
||||||
|
version = "*"
|
||||||
|
|
||||||
sea-orm = { workspace = true }
|
[dependencies.serde]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
secstr = { workspace = true }
|
[dependencies.uuid]
|
||||||
|
version = "1"
|
||||||
serde = { workspace = true }
|
features = ["v1", "v3"]
|
||||||
|
|
||||||
uuid = { workspace = true }
|
|
||||||
|
|
||||||
ldap3_proto = { workspace = true }
|
|
||||||
|
|
||||||
[dependencies.lldap_access_control]
|
[dependencies.lldap_access_control]
|
||||||
path = "../access-control"
|
path = "../access-control"
|
||||||
@@ -57,18 +71,18 @@ path = "../domain-model"
|
|||||||
[dependencies.lldap_opaque_handler]
|
[dependencies.lldap_opaque_handler]
|
||||||
path = "../opaque-handler"
|
path = "../opaque-handler"
|
||||||
|
|
||||||
[dev-dependencies.lldap_domain]
|
|
||||||
path = "../domain"
|
|
||||||
features = ["test"]
|
|
||||||
|
|
||||||
[dev-dependencies.lldap_test_utils]
|
[dev-dependencies.lldap_test_utils]
|
||||||
path = "../test-utils"
|
path = "../test-utils"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
log = { workspace = true }
|
log = "*"
|
||||||
mockall = { workspace = true }
|
mockall = "0.11.4"
|
||||||
pretty_assertions = { workspace = true }
|
pretty_assertions = "1"
|
||||||
|
|
||||||
tokio = { workspace = true }
|
[dev-dependencies.tokio]
|
||||||
|
features = ["full"]
|
||||||
|
version = "1.25"
|
||||||
|
|
||||||
tracing-subscriber = { workspace = true }
|
[dev-dependencies.tracing-subscriber]
|
||||||
|
version = "0.3"
|
||||||
|
features = ["env-filter", "tracing-log"]
|
||||||
|
|||||||
@@ -536,7 +536,7 @@ See https://github.com/lldap/lldap/blob/main/docs/migration_guides/v0.5.md for d
|
|||||||
Conflicting emails:
|
Conflicting emails:
|
||||||
"#,
|
"#,
|
||||||
);
|
);
|
||||||
let duplicate_users = transaction
|
for (email, users) in &transaction
|
||||||
.query_all(
|
.query_all(
|
||||||
builder.build(
|
builder.build(
|
||||||
Query::select()
|
Query::select()
|
||||||
@@ -568,8 +568,9 @@ Conflicting emails:
|
|||||||
row.try_get::<String>("", &Users::Email.to_string())
|
row.try_get::<String>("", &Users::Email.to_string())
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
});
|
})
|
||||||
for (email, users) in &duplicate_users.chunk_by(|(_user, email)| email.to_owned()) {
|
.group_by(|(_user, email)| email.to_owned())
|
||||||
|
{
|
||||||
warn!("Email: {email}");
|
warn!("Email: {email}");
|
||||||
for (user, _email) in users {
|
for (user, _email) in users {
|
||||||
warn!(" User: {}", user.as_str());
|
warn!(" User: {}", user.as_str());
|
||||||
|
|||||||
@@ -29,76 +29,28 @@ impl ReadSchemaBackendHandler for SqlBackendHandler {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl SchemaBackendHandler for SqlBackendHandler {
|
impl SchemaBackendHandler for SqlBackendHandler {
|
||||||
async fn add_user_attribute(&self, request: CreateAttributeRequest) -> Result<()> {
|
async fn add_user_attribute(&self, request: CreateAttributeRequest) -> Result<()> {
|
||||||
self.sql_pool
|
let new_attribute = model::user_attribute_schema::ActiveModel {
|
||||||
.transaction::<_, (), DomainError>(|transaction| {
|
attribute_name: Set(request.name),
|
||||||
Box::pin(async move {
|
attribute_type: Set(request.attribute_type),
|
||||||
// Check for conflicting group attribute with the same name but different type.
|
is_list: Set(request.is_list),
|
||||||
// Done inside the transaction to prevent TOCTOU races.
|
is_user_visible: Set(request.is_visible),
|
||||||
let schema = Self::get_schema_with_transaction(transaction).await?;
|
is_user_editable: Set(request.is_editable),
|
||||||
if let Some(group_attr) = schema
|
is_hardcoded: Set(false),
|
||||||
.group_attributes
|
};
|
||||||
.attributes
|
new_attribute.insert(&self.sql_pool).await?;
|
||||||
.iter()
|
|
||||||
.find(|a| a.name == request.name)
|
|
||||||
&& group_attr.attribute_type != request.attribute_type
|
|
||||||
{
|
|
||||||
return Err(DomainError::InternalError(format!(
|
|
||||||
"A group attribute '{}' already exists with type {:?}, \
|
|
||||||
cannot create a user attribute with type {:?}. \
|
|
||||||
LDAP requires each attribute name to have a single type.",
|
|
||||||
request.name, group_attr.attribute_type, request.attribute_type
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
let new_attribute = model::user_attribute_schema::ActiveModel {
|
|
||||||
attribute_name: Set(request.name),
|
|
||||||
attribute_type: Set(request.attribute_type),
|
|
||||||
is_list: Set(request.is_list),
|
|
||||||
is_user_visible: Set(request.is_visible),
|
|
||||||
is_user_editable: Set(request.is_editable),
|
|
||||||
is_hardcoded: Set(false),
|
|
||||||
};
|
|
||||||
new_attribute.insert(transaction).await?;
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn add_group_attribute(&self, request: CreateAttributeRequest) -> Result<()> {
|
async fn add_group_attribute(&self, request: CreateAttributeRequest) -> Result<()> {
|
||||||
self.sql_pool
|
let new_attribute = model::group_attribute_schema::ActiveModel {
|
||||||
.transaction::<_, (), DomainError>(|transaction| {
|
attribute_name: Set(request.name),
|
||||||
Box::pin(async move {
|
attribute_type: Set(request.attribute_type),
|
||||||
// Check for conflicting user attribute with the same name but different type.
|
is_list: Set(request.is_list),
|
||||||
// Done inside the transaction to prevent TOCTOU races.
|
is_group_visible: Set(request.is_visible),
|
||||||
let schema = Self::get_schema_with_transaction(transaction).await?;
|
is_group_editable: Set(request.is_editable),
|
||||||
if let Some(user_attr) = schema
|
is_hardcoded: Set(false),
|
||||||
.user_attributes
|
};
|
||||||
.attributes
|
new_attribute.insert(&self.sql_pool).await?;
|
||||||
.iter()
|
|
||||||
.find(|a| a.name == request.name)
|
|
||||||
&& user_attr.attribute_type != request.attribute_type
|
|
||||||
{
|
|
||||||
return Err(DomainError::InternalError(format!(
|
|
||||||
"A user attribute '{}' already exists with type {:?}, \
|
|
||||||
cannot create a group attribute with type {:?}. \
|
|
||||||
LDAP requires each attribute name to have a single type.",
|
|
||||||
request.name, user_attr.attribute_type, request.attribute_type
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
let new_attribute = model::group_attribute_schema::ActiveModel {
|
|
||||||
attribute_name: Set(request.name),
|
|
||||||
attribute_type: Set(request.attribute_type),
|
|
||||||
is_list: Set(request.is_list),
|
|
||||||
is_group_visible: Set(request.is_visible),
|
|
||||||
is_group_editable: Set(request.is_editable),
|
|
||||||
is_hardcoded: Set(false),
|
|
||||||
};
|
|
||||||
new_attribute.insert(transaction).await?;
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lldap_test_utils"
|
name = "lldap_test_utils"
|
||||||
version = "0.1.1"
|
version = "0.1.0"
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
homepage.workspace = true
|
homepage.workspace = true
|
||||||
@@ -9,13 +9,14 @@ repository.workspace = true
|
|||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait = { workspace = true }
|
async-trait = "0.1"
|
||||||
mockall = { workspace = true }
|
ldap3_proto = "0.6.0"
|
||||||
tracing = { workspace = true }
|
mockall = "0.11.4"
|
||||||
|
tracing = "*"
|
||||||
|
|
||||||
uuid = { workspace = true }
|
[dependencies.uuid]
|
||||||
|
version = "1"
|
||||||
ldap3_proto = { workspace = true }
|
features = ["v1", "v3"]
|
||||||
|
|
||||||
[dependencies.lldap_access_control]
|
[dependencies.lldap_access_control]
|
||||||
path = "../access-control"
|
path = "../access-control"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lldap_validation"
|
name = "lldap_validation"
|
||||||
version = "0.6.3"
|
version = "0.6.0"
|
||||||
authors = ["Simon Broeng Jensen <sbj@cwconsult.dk>"]
|
authors = ["Simon Broeng Jensen <sbj@cwconsult.dk>"]
|
||||||
description = "Validation logic for LLDAP"
|
description = "Validation logic for LLDAP"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
|||||||
@@ -86,7 +86,6 @@ services:
|
|||||||
# - LLDAP_SMTP_OPTIONS__PASSWORD=PasswordGoesHere # The SMTP password
|
# - LLDAP_SMTP_OPTIONS__PASSWORD=PasswordGoesHere # The SMTP password
|
||||||
# - LLDAP_SMTP_OPTIONS__FROM=no-reply <no-reply@example.com> # The header field, optional: how the sender appears in the email. The first is a free-form name, followed by an email between <>.
|
# - LLDAP_SMTP_OPTIONS__FROM=no-reply <no-reply@example.com> # The header field, optional: how the sender appears in the email. The first is a free-form name, followed by an email between <>.
|
||||||
# - LLDAP_SMTP_OPTIONS__TO=admin <admin@example.com> # Same for reply-to, optional.
|
# - LLDAP_SMTP_OPTIONS__TO=admin <admin@example.com> # Same for reply-to, optional.
|
||||||
# - LLDAP_HTTP_URL=https://lldap.example.com # URL used in the email template to compose the reset link
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Then the service will listen on two ports, one for LDAP and one for the web
|
Then the service will listen on two ports, one for LDAP and one for the web
|
||||||
|
|||||||
@@ -19,13 +19,11 @@ configuration files:
|
|||||||
- [Dolibarr](dolibarr.md)
|
- [Dolibarr](dolibarr.md)
|
||||||
- [Duo Auth Proxy](duo_auth_proxy.md)
|
- [Duo Auth Proxy](duo_auth_proxy.md)
|
||||||
- [Ejabberd](ejabberd.md)
|
- [Ejabberd](ejabberd.md)
|
||||||
- [Elasticsearch](elasticsearch.md)
|
|
||||||
- [Emby](emby.md)
|
- [Emby](emby.md)
|
||||||
- [Ergo IRCd](ergo.md)
|
- [Ergo IRCd](ergo.md)
|
||||||
- [Gerrit](gerrit.md)
|
- [Gerrit](gerrit.md)
|
||||||
- [Gitea](gitea.md)
|
- [Gitea](gitea.md)
|
||||||
- [GitLab](gitlab.md)
|
- [GitLab](gitlab.md)
|
||||||
- [Gogs](gogs.md)
|
|
||||||
- [Grafana](grafana_ldap_config.toml)
|
- [Grafana](grafana_ldap_config.toml)
|
||||||
- [Grocy](grocy.md)
|
- [Grocy](grocy.md)
|
||||||
- [Harbor](harbor.md)
|
- [Harbor](harbor.md)
|
||||||
@@ -52,7 +50,6 @@ configuration files:
|
|||||||
- [Nexus](nexus.md)
|
- [Nexus](nexus.md)
|
||||||
- [OCIS (OwnCloud Infinite Scale)](ocis.md)
|
- [OCIS (OwnCloud Infinite Scale)](ocis.md)
|
||||||
- [OneDev](onedev.md)
|
- [OneDev](onedev.md)
|
||||||
- [OpenCloud](opencloud.md)
|
|
||||||
- [Organizr](Organizr.md)
|
- [Organizr](Organizr.md)
|
||||||
- [Peertube](peertube.md)
|
- [Peertube](peertube.md)
|
||||||
- [Penpot](penpot.md)
|
- [Penpot](penpot.md)
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
# Elasticsearch configuration
|
|
||||||
|
|
||||||
> ⚠️ Configuring Elasticsearch to use LDAP auth requires a paid licence. Only the `default` and `file` realms are enabled on a Basic licence. Using the trial licence on cluster init will allow LDAP auth to be configured.
|
|
||||||
|
|
||||||
This basic configuration example is for LLDAP auth on [Elastic Cloud on Kubernetes (ECK)](https://www.elastic.co/docs/deploy-manage/deploy/cloud-on-k8s). Advanced configuration can be found in the [Elastic docs](https://www.elastic.co/docs/deploy-manage/users-roles/cluster-or-deployment-auth/ldap).
|
|
||||||
|
|
||||||
## Elasticsearch
|
|
||||||
|
|
||||||
To perform auth using LLDAP in Elasticsearch, add the following lines to the Elasticsearch spec:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
spec:
|
|
||||||
nodesets:
|
|
||||||
- name: elasticsearch
|
|
||||||
config:
|
|
||||||
xpack.security.authc.realms.ldap:
|
|
||||||
ldap1:
|
|
||||||
order: 1
|
|
||||||
enabled: true
|
|
||||||
url: "ldap://<ip.of.lldap.instance>:3890"
|
|
||||||
user_dn_templates:
|
|
||||||
- "uid={0},ou=people,dc=example,dc=com"
|
|
||||||
bind_dn: "uid=admin,ou=people,dc=example,dc=com"
|
|
||||||
group_search:
|
|
||||||
base_dn: "ou=groups,dc=example,dc=com"
|
|
||||||
unmapped_groups_as_roles: false
|
|
||||||
secureSettings:
|
|
||||||
- secretName: elasticsearch-keystore-values
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, create a secret called `elasticsearch-keystore-values`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Secret
|
|
||||||
type: Opaque
|
|
||||||
metadata:
|
|
||||||
name: elasticsearch-keystore-values
|
|
||||||
namespace: elastic
|
|
||||||
data:
|
|
||||||
xpack.security.authc.realms.ldap.ldap1.secure_bind_password: base64_encoded_ldap_admin_password
|
|
||||||
```
|
|
||||||
|
|
||||||
## Kibana
|
|
||||||
|
|
||||||
To allow Kibana to auth logins using LLDAP, add the following lines to the Kibana spec:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
spec:
|
|
||||||
config:
|
|
||||||
xpack.security.authc.providers:
|
|
||||||
basic.ldap1:
|
|
||||||
order: 0
|
|
||||||
```
|
|
||||||
|
|
||||||
Unless doing additional manifest configuration to automatically map, you will need to create a role mapping between an LLDAP role and an Elasticsearch role (e.g. `superuser`). This can be done by logging in using the default `elastic` user created during cluster init and then creating a role mapping in Stack Management. Once created, you will be able to login using LLDAP auth.
|
|
||||||
+1
-16
@@ -1,21 +1,6 @@
|
|||||||
# Gogs LDAP configuration
|
# Gogs LDAP configuration
|
||||||
|
|
||||||
## Via Simple Auth (easier)
|
Gogs can make use of LDAP and therefore lldap.
|
||||||
|
|
||||||
Go to the Administration settings, then go to Authentication. There, you have to add an authentication source.
|
|
||||||
|
|
||||||
For type, select "LDAP (Simple Auth)".
|
|
||||||
Name your authentication source however you'd like.
|
|
||||||
It is up to you to select your security protocol, but the only two compatible options are "LDAPS" and "Unencrypted".
|
|
||||||
As your host, put in the IP or FQDN (if you have DNS).
|
|
||||||
As your port, check your configuration. It will generally be 3890 for unencrypted (once again check your config/docker compose files), and 6360 for LDAPS (once again check your config/docker compose files).
|
|
||||||
Your User DN should follow this pattern: `uid=%s,ou=people,<your_base_dn>` (for example, `uid=%s,ou=people,dc=example,dc=com`). Replace `<your_base_dn>` with your actual base DN.
|
|
||||||
It is recommended to have your user filter to be `(&(objectClass=person)(uid=%s))`.
|
|
||||||
Set username attribute to `uid`, Given Name to `givenName`, surname to `sn`, and email to `mail`
|
|
||||||
|
|
||||||
You can (and should if you don't know LDAP) leave the rest empty.
|
|
||||||
|
|
||||||
## Via Bind DN (more complicated)
|
|
||||||
|
|
||||||
The following configuration is adapted from the example configuration at [their repository](https://github.com/gogs/gogs/blob/main/conf/auth.d/ldap_bind_dn.conf.example).
|
The following configuration is adapted from the example configuration at [their repository](https://github.com/gogs/gogs/blob/main/conf/auth.d/ldap_bind_dn.conf.example).
|
||||||
The example is a container configuration - the file should live within `conf/auth.d/some_name.conf`:
|
The example is a container configuration - the file should live within `conf/auth.d/some_name.conf`:
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
# OpenCloud example config
|
|
||||||
|
|
||||||
|
|
||||||
## About OpenCloud
|
|
||||||
|
|
||||||
A light-weight file-hosting / webDAV service written in Go and forked from ownCloud Infinite Scale (oCIS).
|
|
||||||
|
|
||||||
More information:
|
|
||||||
* https://opencloud.eu
|
|
||||||
* https://github.com/opencloud-eu
|
|
||||||
|
|
||||||
|
|
||||||
## LLDAP Configuration
|
|
||||||
|
|
||||||
OpenCloud ships an OIDC provider and a built-in LDAP server. It officially supports using a third-party OIDC provider.
|
|
||||||
|
|
||||||
This is **not** what this config does. This config leaves the general auth/OIDC infrastructure in place, but replaces the LDAP server from underneath it with LLDAP.
|
|
||||||
|
|
||||||
Configuration happens via environment variables. On FreeBSD, these are provided via `/usr/local/etc/opencloud/config.env`; on Linux you can provide them via the Docker configuration.
|
|
||||||
|
|
||||||
|
|
||||||
```dotenv
|
|
||||||
# Replace with actual IP and Port
|
|
||||||
OC_LDAP_URI=ldap://<lldap_ip>:3890
|
|
||||||
# Remove the following if you use LDAPS and your cert is not self-signed
|
|
||||||
OC_LDAP_INSECURE="true"
|
|
||||||
|
|
||||||
# Replace with your bind-user; can be in
|
|
||||||
OC_LDAP_BIND_DN="cn=<bind_user>,ou=people,dc=example,dc=com"
|
|
||||||
OC_LDAP_BIND_PASSWORD="<secret>"
|
|
||||||
|
|
||||||
OC_LDAP_GROUP_BASE_DN="ou=groups,dc=example,dc=com"
|
|
||||||
OC_LDAP_GROUP_SCHEMA_ID=entryuuid
|
|
||||||
|
|
||||||
OC_LDAP_USER_BASE_DN="ou=people,dc=example,dc=com"
|
|
||||||
OC_LDAP_USER_SCHEMA_ID=entryuuid
|
|
||||||
|
|
||||||
# Only allow users from specific group to login; remove this if everyone's allowed
|
|
||||||
OC_LDAP_USER_FILTER='(&(objectClass=person)(memberOf=cn=<opencloud_users>,ou=groups,dc=example,dc=com))'
|
|
||||||
|
|
||||||
# Other options have not been tested
|
|
||||||
OC_LDAP_DISABLE_USER_MECHANISM="none"
|
|
||||||
|
|
||||||
# If you bind-user is in lldap_strict_readonly set to false (this hides "forgot password"-buttons)
|
|
||||||
OC_LDAP_SERVER_WRITE_ENABLED="false"
|
|
||||||
# If your bind-user can change passwords:
|
|
||||||
OC_LDAP_SERVER_WRITE_ENABLED="true" # Not tested, yet!
|
|
||||||
|
|
||||||
# Don't start built-in LDAP, because it's replaced by LLDAP
|
|
||||||
OC_EXCLUDE_RUN_SERVICES="idm"
|
|
||||||
```
|
|
||||||
|
|
||||||
There is currently no (documented) way to give an LDAP user (or group) admin rights in OpenCloud.
|
|
||||||
|
|
||||||
See also [the official LDAP documentation](https://github.com/opencloud-eu/opencloud/blob/main/devtools/deployments/opencloud_full/ldap.yml).
|
|
||||||
@@ -48,13 +48,3 @@ To integrate with LLDAP,
|
|||||||
allow-invalid-certs = true
|
allow-invalid-certs = true
|
||||||
enable = false
|
enable = false
|
||||||
```
|
```
|
||||||
|
|
||||||
## Email alias
|
|
||||||
If you want to enable [email aliases](https://stalw.art/docs/mta/inbound/rcpt/#catch-all-addresses), you have to create a new *User-defined attribute* under *User schema* of type string. Currently, LLDAP doesn't support multi-value filters. If you want multiple aliases, you will have to create multiple attributes (`mailAlias1`, `mailAlias2`, ..., `mailAliasN`), where `N` is the maximum number of aliases an account will have.
|
|
||||||
|
|
||||||
You also need to change your ldap filter for emails.
|
|
||||||
```toml
|
|
||||||
[directory.ldap.filter]
|
|
||||||
# Add one clause per alias attribute you created (example: mailAlias1..mailAlias3)
|
|
||||||
email = "(&(objectclass=person)(|(mail=?)(mailAlias1=?)(mailAlias2=?)(mailAlias3=?)))"
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ Unique Identifier Attribute - entryUUID
|
|||||||
- Group Object Filter - objectClass=groupOfUniqueNames
|
- Group Object Filter - objectClass=groupOfUniqueNames
|
||||||
|
|
||||||
Member Attribute
|
Member Attribute
|
||||||
uniqueMember
|
member
|
||||||
|
|
||||||
Validate Attribute
|
Validate Attribute
|
||||||
enter a user e-mail address who has been added in LLDAP , and click test configuration, test show be successful
|
enter a user e-mail address who has been added in LLDAP , and click test configuration, test show be successful
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
# MSRV from the project
|
# MSRV from the project
|
||||||
rustVersion = "1.91.0";
|
rustVersion = "1.89.0";
|
||||||
|
|
||||||
# Rust toolchain with required components
|
# Rust toolchain with required components
|
||||||
rustToolchain = pkgs.rust-bin.stable.${rustVersion}.default.override {
|
rustToolchain = pkgs.rust-bin.stable.${rustVersion}.default.override {
|
||||||
@@ -159,4 +159,4 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
+18
-11
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lldap_migration_tool"
|
name = "lldap_migration_tool"
|
||||||
version = "0.4.3"
|
version = "0.4.2"
|
||||||
description = "CLI migration tool to go from OpenLDAP to LLDAP"
|
description = "CLI migration tool to go from OpenLDAP to LLDAP"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
@@ -10,24 +10,31 @@ repository.workspace = true
|
|||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { workspace = true }
|
anyhow = "*"
|
||||||
base64 = { workspace = true }
|
base64 = "0.13"
|
||||||
ldap3 = { workspace = true }
|
rand = "0.8"
|
||||||
rand = { workspace = true }
|
requestty = "0.4.1"
|
||||||
requestty = "0.6"
|
serde_json = "1"
|
||||||
serde = { workspace = true }
|
smallvec = "*"
|
||||||
serde_json = { workspace = true }
|
|
||||||
smallvec = "1"
|
|
||||||
|
|
||||||
[dependencies.lldap_auth]
|
[dependencies.lldap_auth]
|
||||||
path = "../crates/auth"
|
path = "../crates/auth"
|
||||||
features = ["opaque_client"]
|
features = ["opaque_client"]
|
||||||
|
|
||||||
[dependencies.graphql_client]
|
[dependencies.graphql_client]
|
||||||
workspace = true
|
|
||||||
features = ["graphql_query_derive", "reqwest-rustls"]
|
features = ["graphql_query_derive", "reqwest-rustls"]
|
||||||
|
default-features = false
|
||||||
|
version = "0.11"
|
||||||
|
|
||||||
[dependencies.reqwest]
|
[dependencies.reqwest]
|
||||||
workspace = true
|
version = "*"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["json", "blocking", "rustls-tls"]
|
features = ["json", "blocking", "rustls-tls"]
|
||||||
|
|
||||||
|
[dependencies.ldap3]
|
||||||
|
version = "*"
|
||||||
|
default-features = false
|
||||||
|
features = ["sync", "tls-rustls-ring"]
|
||||||
|
|
||||||
|
[dependencies.serde]
|
||||||
|
workspace = true
|
||||||
|
|||||||
@@ -193,9 +193,7 @@ impl TryFrom<ResultEntry> for User {
|
|||||||
display_name,
|
display_name,
|
||||||
first_name,
|
first_name,
|
||||||
last_name,
|
last_name,
|
||||||
avatar: avatar.map(|avatar| {
|
avatar: avatar.map(base64::encode),
|
||||||
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, avatar)
|
|
||||||
}),
|
|
||||||
attributes: None,
|
attributes: None,
|
||||||
},
|
},
|
||||||
password,
|
password,
|
||||||
|
|||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "1.91.0"
|
channel = "1.89.0"
|
||||||
|
|||||||
Generated
+124
-137
@@ -1,33 +1,63 @@
|
|||||||
schema {
|
type AttributeValue {
|
||||||
query: Query
|
name: String!
|
||||||
mutation: Mutation
|
value: [String!]!
|
||||||
|
schema: AttributeSchema!
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AttributeType {
|
type Mutation {
|
||||||
STRING
|
createUser(user: CreateUserInput!): User!
|
||||||
INTEGER
|
createGroup(name: String!): Group!
|
||||||
JPEG_PHOTO
|
createGroupWithDetails(request: CreateGroupInput!): Group!
|
||||||
DATE_TIME
|
updateUser(user: UpdateUserInput!): Success!
|
||||||
|
updateGroup(group: UpdateGroupInput!): Success!
|
||||||
|
addUserToGroup(userId: String!, groupId: Int!): Success!
|
||||||
|
removeUserFromGroup(userId: String!, groupId: Int!): Success!
|
||||||
|
deleteUser(userId: String!): Success!
|
||||||
|
deleteGroup(groupId: Int!): Success!
|
||||||
|
addUserAttribute(name: String!, attributeType: AttributeType!, isList: Boolean!, isVisible: Boolean!, isEditable: Boolean!): Success!
|
||||||
|
addGroupAttribute(name: String!, attributeType: AttributeType!, isList: Boolean!, isVisible: Boolean!, isEditable: Boolean!): Success!
|
||||||
|
deleteUserAttribute(name: String!): Success!
|
||||||
|
deleteGroupAttribute(name: String!): Success!
|
||||||
|
addUserObjectClass(name: String!): Success!
|
||||||
|
addGroupObjectClass(name: String!): Success!
|
||||||
|
deleteUserObjectClass(name: String!): Success!
|
||||||
|
deleteGroupObjectClass(name: String!): Success!
|
||||||
}
|
}
|
||||||
|
|
||||||
input AttributeValueInput {
|
type Group {
|
||||||
"""
|
id: Int!
|
||||||
The name of the attribute. It must be present in the schema, and the type informs how
|
|
||||||
to interpret the values.
|
|
||||||
""" name: String!
|
|
||||||
"""
|
|
||||||
The values of the attribute.
|
|
||||||
If the attribute is not a list, the vector must contain exactly one element.
|
|
||||||
Integers (signed 64 bits) are represented as strings.
|
|
||||||
Dates are represented as strings in RFC3339 format, e.g. "2019-10-12T07:20:50.52Z".
|
|
||||||
JpegPhotos are represented as base64 encoded strings. They must be valid JPEGs.
|
|
||||||
""" value: [String!]!
|
|
||||||
}
|
|
||||||
|
|
||||||
"The details required to create a group."
|
|
||||||
input CreateGroupInput {
|
|
||||||
displayName: String!
|
displayName: String!
|
||||||
"User-defined attributes." attributes: [AttributeValueInput!]
|
creationDate: DateTimeUtc!
|
||||||
|
uuid: String!
|
||||||
|
"User-defined attributes."
|
||||||
|
attributes: [AttributeValue!]!
|
||||||
|
"The groups to which this user belongs."
|
||||||
|
users: [User!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
A filter for requests, specifying a boolean expression based on field constraints. Only one of
|
||||||
|
the fields can be set at a time.
|
||||||
|
"""
|
||||||
|
input RequestFilter {
|
||||||
|
any: [RequestFilter!]
|
||||||
|
all: [RequestFilter!]
|
||||||
|
not: RequestFilter
|
||||||
|
eq: EqualityConstraint
|
||||||
|
memberOf: String
|
||||||
|
memberOfId: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
"DateTime"
|
||||||
|
scalar DateTimeUtc
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
apiVersion: String!
|
||||||
|
user(userId: String!): User!
|
||||||
|
users(filters: RequestFilter): [User!]!
|
||||||
|
groups: [Group!]!
|
||||||
|
group(groupId: Int!): Group!
|
||||||
|
schema: Schema!
|
||||||
}
|
}
|
||||||
|
|
||||||
"The details required to create a user."
|
"The details required to create a user."
|
||||||
@@ -50,36 +80,19 @@ input CreateUserInput {
|
|||||||
"Attributes." attributes: [AttributeValueInput!]
|
"Attributes." attributes: [AttributeValueInput!]
|
||||||
}
|
}
|
||||||
|
|
||||||
input EqualityConstraint {
|
type ObjectClassInfo {
|
||||||
field: String!
|
objectClass: String!
|
||||||
value: String!
|
isHardcoded: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
"""
|
type AttributeSchema {
|
||||||
A filter for requests, specifying a boolean expression based on field constraints. Only one of
|
name: String!
|
||||||
the fields can be set at a time.
|
attributeType: AttributeType!
|
||||||
"""
|
isList: Boolean!
|
||||||
input RequestFilter {
|
isVisible: Boolean!
|
||||||
any: [RequestFilter!]
|
isEditable: Boolean!
|
||||||
all: [RequestFilter!]
|
isHardcoded: Boolean!
|
||||||
not: RequestFilter
|
isReadonly: Boolean!
|
||||||
eq: EqualityConstraint
|
|
||||||
memberOf: String
|
|
||||||
memberOfId: Int
|
|
||||||
}
|
|
||||||
|
|
||||||
"The fields that can be updated for a group."
|
|
||||||
input UpdateGroupInput {
|
|
||||||
"The group ID." id: Int!
|
|
||||||
"The new display name." displayName: String
|
|
||||||
"""
|
|
||||||
Attribute names to remove.
|
|
||||||
They are processed before insertions.
|
|
||||||
""" removeAttributes: [String!]
|
|
||||||
"""
|
|
||||||
Inserts or updates the given attributes.
|
|
||||||
For lists, the entire list must be provided.
|
|
||||||
""" insertAttributes: [AttributeValueInput!]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
"The fields that can be updated for a user."
|
"The fields that can be updated for a user."
|
||||||
@@ -109,87 +122,9 @@ input UpdateUserInput {
|
|||||||
""" insertAttributes: [AttributeValueInput!]
|
""" insertAttributes: [AttributeValueInput!]
|
||||||
}
|
}
|
||||||
|
|
||||||
"""
|
input EqualityConstraint {
|
||||||
Combined date and time (with time zone) in [RFC 3339][0] format.
|
field: String!
|
||||||
|
value: String!
|
||||||
Represents a description of an exact instant on the time-line (such as the
|
|
||||||
instant that a user account was created).
|
|
||||||
|
|
||||||
[`DateTime` scalar][1] compliant.
|
|
||||||
|
|
||||||
See also [`chrono::DateTime`][2] for details.
|
|
||||||
|
|
||||||
[0]: https://datatracker.ietf.org/doc/html/rfc3339#section-5
|
|
||||||
[1]: https://graphql-scalars.dev/docs/scalars/date-time
|
|
||||||
[2]: https://docs.rs/chrono/latest/chrono/struct.DateTime.html
|
|
||||||
"""
|
|
||||||
scalar DateTime
|
|
||||||
|
|
||||||
type AttributeList {
|
|
||||||
attributes: [AttributeSchema!]!
|
|
||||||
extraLdapObjectClasses: [String!]!
|
|
||||||
ldapObjectClasses: [ObjectClassInfo!]!
|
|
||||||
}
|
|
||||||
|
|
||||||
type AttributeSchema {
|
|
||||||
name: String!
|
|
||||||
attributeType: AttributeType!
|
|
||||||
isList: Boolean!
|
|
||||||
isVisible: Boolean!
|
|
||||||
isEditable: Boolean!
|
|
||||||
isHardcoded: Boolean!
|
|
||||||
isReadonly: Boolean!
|
|
||||||
}
|
|
||||||
|
|
||||||
type AttributeValue {
|
|
||||||
name: String!
|
|
||||||
value: [String!]!
|
|
||||||
schema: AttributeSchema!
|
|
||||||
}
|
|
||||||
|
|
||||||
type Group {
|
|
||||||
id: Int!
|
|
||||||
displayName: String!
|
|
||||||
creationDate: DateTime!
|
|
||||||
uuid: String!
|
|
||||||
"User-defined attributes."
|
|
||||||
attributes: [AttributeValue!]!
|
|
||||||
"The groups to which this user belongs."
|
|
||||||
users: [User!]!
|
|
||||||
}
|
|
||||||
|
|
||||||
type Mutation {
|
|
||||||
createUser(user: CreateUserInput!): User!
|
|
||||||
createGroup(name: String!): Group!
|
|
||||||
createGroupWithDetails(request: CreateGroupInput!): Group!
|
|
||||||
updateUser(user: UpdateUserInput!): Success!
|
|
||||||
updateGroup(group: UpdateGroupInput!): Success!
|
|
||||||
addUserToGroup(userId: String!, groupId: Int!): Success!
|
|
||||||
removeUserFromGroup(userId: String!, groupId: Int!): Success!
|
|
||||||
deleteUser(userId: String!): Success!
|
|
||||||
deleteGroup(groupId: Int!): Success!
|
|
||||||
addUserAttribute(name: String!, attributeType: AttributeType!, isList: Boolean!, isVisible: Boolean!, isEditable: Boolean!): Success!
|
|
||||||
addGroupAttribute(name: String!, attributeType: AttributeType!, isList: Boolean!, isVisible: Boolean!, isEditable: Boolean!): Success!
|
|
||||||
deleteUserAttribute(name: String!): Success!
|
|
||||||
deleteGroupAttribute(name: String!): Success!
|
|
||||||
addUserObjectClass(name: String!): Success!
|
|
||||||
addGroupObjectClass(name: String!): Success!
|
|
||||||
deleteUserObjectClass(name: String!): Success!
|
|
||||||
deleteGroupObjectClass(name: String!): Success!
|
|
||||||
}
|
|
||||||
|
|
||||||
type ObjectClassInfo {
|
|
||||||
objectClass: String!
|
|
||||||
isHardcoded: Boolean!
|
|
||||||
}
|
|
||||||
|
|
||||||
type Query {
|
|
||||||
apiVersion: String!
|
|
||||||
user(userId: String!): User!
|
|
||||||
users(filters: RequestFilter): [User!]!
|
|
||||||
groups: [Group!]!
|
|
||||||
group(groupId: Int!): Group!
|
|
||||||
schema: Schema!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Schema {
|
type Schema {
|
||||||
@@ -197,8 +132,38 @@ type Schema {
|
|||||||
groupSchema: AttributeList!
|
groupSchema: AttributeList!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Success {
|
"The fields that can be updated for a group."
|
||||||
ok: Boolean!
|
input UpdateGroupInput {
|
||||||
|
"The group ID." id: Int!
|
||||||
|
"The new display name." displayName: String
|
||||||
|
"""
|
||||||
|
Attribute names to remove.
|
||||||
|
They are processed before insertions.
|
||||||
|
""" removeAttributes: [String!]
|
||||||
|
"""
|
||||||
|
Inserts or updates the given attributes.
|
||||||
|
For lists, the entire list must be provided.
|
||||||
|
""" insertAttributes: [AttributeValueInput!]
|
||||||
|
}
|
||||||
|
|
||||||
|
input AttributeValueInput {
|
||||||
|
"""
|
||||||
|
The name of the attribute. It must be present in the schema, and the type informs how
|
||||||
|
to interpret the values.
|
||||||
|
""" name: String!
|
||||||
|
"""
|
||||||
|
The values of the attribute.
|
||||||
|
If the attribute is not a list, the vector must contain exactly one element.
|
||||||
|
Integers (signed 64 bits) are represented as strings.
|
||||||
|
Dates are represented as strings in RFC3339 format, e.g. "2019-10-12T07:20:50.52Z".
|
||||||
|
JpegPhotos are represented as base64 encoded strings. They must be valid JPEGs.
|
||||||
|
""" value: [String!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
"The details required to create a group."
|
||||||
|
input CreateGroupInput {
|
||||||
|
displayName: String!
|
||||||
|
"User-defined attributes." attributes: [AttributeValueInput!]
|
||||||
}
|
}
|
||||||
|
|
||||||
type User {
|
type User {
|
||||||
@@ -208,10 +173,32 @@ type User {
|
|||||||
firstName: String!
|
firstName: String!
|
||||||
lastName: String!
|
lastName: String!
|
||||||
avatar: String
|
avatar: String
|
||||||
creationDate: DateTime!
|
creationDate: DateTimeUtc!
|
||||||
uuid: String!
|
uuid: String!
|
||||||
"User-defined attributes."
|
"User-defined attributes."
|
||||||
attributes: [AttributeValue!]!
|
attributes: [AttributeValue!]!
|
||||||
"The groups to which this user belongs."
|
"The groups to which this user belongs."
|
||||||
groups: [Group!]!
|
groups: [Group!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum AttributeType {
|
||||||
|
STRING
|
||||||
|
INTEGER
|
||||||
|
JPEG_PHOTO
|
||||||
|
DATE_TIME
|
||||||
|
}
|
||||||
|
|
||||||
|
type AttributeList {
|
||||||
|
attributes: [AttributeSchema!]!
|
||||||
|
extraLdapObjectClasses: [String!]!
|
||||||
|
ldapObjectClasses: [ObjectClassInfo!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Success {
|
||||||
|
ok: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
|
schema {
|
||||||
|
query: Query
|
||||||
|
mutation: Mutation
|
||||||
|
}
|
||||||
|
|||||||
@@ -712,9 +712,9 @@ main() {
|
|||||||
redundant_users="$(printf '%s' "$redundant_users" | jq --compact-output --arg id "$id" '. - [$id]')"
|
redundant_users="$(printf '%s' "$redundant_users" | jq --compact-output --arg id "$id" '. - [$id]')"
|
||||||
|
|
||||||
if [[ "$password_file" != 'null' ]] && [[ "$password_file" != '""' ]]; then
|
if [[ "$password_file" != 'null' ]] && [[ "$password_file" != '""' ]]; then
|
||||||
LLDAP_USER_PASSWORD="$(< "$password_file")" "$LLDAP_SET_PASSWORD_PATH" --base-url "$LLDAP_URL" --token "$TOKEN" --username "$id"
|
"$LLDAP_SET_PASSWORD_PATH" --base-url "$LLDAP_URL" --token "$TOKEN" --username "$id" --password "$(cat $password_file)"
|
||||||
elif [[ "$password" != 'null' ]] && [[ "$password" != '""' ]]; then
|
elif [[ "$password" != 'null' ]] && [[ "$password" != '""' ]]; then
|
||||||
LLDAP_USER_PASSWORD="$password" "$LLDAP_SET_PASSWORD_PATH" --base-url "$LLDAP_URL" --token "$TOKEN" --username "$id"
|
"$LLDAP_SET_PASSWORD_PATH" --base-url "$LLDAP_URL" --token "$TOKEN" --username "$id" --password "$password"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Process custom attributes
|
# Process custom attributes
|
||||||
|
|||||||
+96
-43
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lldap"
|
name = "lldap"
|
||||||
version = "0.6.3"
|
version = "0.6.2"
|
||||||
description = "Super-simple and lightweight LDAP server"
|
description = "Super-simple and lightweight LDAP server"
|
||||||
categories = ["authentication", "command-line-utilities"]
|
categories = ["authentication", "command-line-utilities"]
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
@@ -19,46 +19,35 @@ actix-rt = "2"
|
|||||||
actix-server = "2"
|
actix-server = "2"
|
||||||
actix-service = "2"
|
actix-service = "2"
|
||||||
actix-web-httpauth = "0.8"
|
actix-web-httpauth = "0.8"
|
||||||
anyhow = { workspace = true }
|
anyhow = "*"
|
||||||
async-trait = { workspace = true }
|
async-trait = "0.1"
|
||||||
bincode = { workspace = true }
|
bincode = "1.3"
|
||||||
chrono = { workspace = true }
|
cron = "*"
|
||||||
clap = { workspace = true }
|
derive_builder = "0.12"
|
||||||
cron = "0"
|
|
||||||
derive_more = { workspace = true }
|
|
||||||
derive_builder = "0.20"
|
|
||||||
figment_file_provider_adapter = "0.1"
|
figment_file_provider_adapter = "0.1"
|
||||||
futures = "0"
|
futures = "*"
|
||||||
futures-util = "0"
|
futures-util = "*"
|
||||||
hmac = "0.12"
|
hmac = "0.12"
|
||||||
juniper = { workspace = true }
|
http = "*"
|
||||||
jwt = { workspace = true }
|
juniper = "0.15"
|
||||||
ldap3_proto = { workspace = true }
|
jwt = "0.16"
|
||||||
log = { workspace = true }
|
ldap3_proto = "0.6.0"
|
||||||
opaque-ke = { workspace = true }
|
log = "*"
|
||||||
rand = { workspace = true }
|
|
||||||
rand_chacha = "0.3"
|
rand_chacha = "0.3"
|
||||||
rustls-pemfile = "2"
|
rustls-pemfile = "2"
|
||||||
sea-orm = { workspace = true }
|
serde_json = "1"
|
||||||
secstr = { workspace = true }
|
|
||||||
serde = { workspace = true }
|
|
||||||
serde_json = { workspace = true }
|
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
strum = { workspace = true }
|
thiserror = "2"
|
||||||
thiserror = { workspace = true }
|
|
||||||
time = "0.3"
|
time = "0.3"
|
||||||
tokio = { workspace = true }
|
|
||||||
tokio-rustls = "0.26"
|
tokio-rustls = "0.26"
|
||||||
tokio-stream = "0"
|
tokio-stream = "*"
|
||||||
tokio-util = "0.7"
|
tokio-util = "0.7"
|
||||||
tracing = { workspace = true }
|
tracing = "*"
|
||||||
tracing-actix-web = "0.7"
|
tracing-actix-web = "0.7"
|
||||||
tracing-attributes = "^0.1.21"
|
tracing-attributes = "^0.1.21"
|
||||||
tracing-log = "0"
|
tracing-log = "*"
|
||||||
tracing-subscriber = { workspace = true }
|
urlencoding = "2"
|
||||||
urlencoding = { workspace = true }
|
webpki-roots = "0.26"
|
||||||
uuid = { workspace = true }
|
|
||||||
webpki-roots = "1"
|
|
||||||
|
|
||||||
[dependencies.actix-web]
|
[dependencies.actix-web]
|
||||||
features = ["rustls-0_23"]
|
features = ["rustls-0_23"]
|
||||||
@@ -73,9 +62,26 @@ version = "0.23"
|
|||||||
features = ["std"]
|
features = ["std"]
|
||||||
version = "1"
|
version = "1"
|
||||||
|
|
||||||
|
[dependencies.chrono]
|
||||||
|
features = ["serde"]
|
||||||
|
version = "*"
|
||||||
|
|
||||||
|
[dependencies.clap]
|
||||||
|
features = ["std", "color", "suggestions", "derive", "env"]
|
||||||
|
version = "4"
|
||||||
|
|
||||||
|
[dependencies.derive_more]
|
||||||
|
features = ["debug", "display", "from", "from_str"]
|
||||||
|
default-features = false
|
||||||
|
version = "1"
|
||||||
|
|
||||||
[dependencies.figment]
|
[dependencies.figment]
|
||||||
features = ["env", "toml"]
|
features = ["env", "toml"]
|
||||||
version = "0"
|
version = "*"
|
||||||
|
|
||||||
|
[dependencies.tracing-subscriber]
|
||||||
|
version = "0.3"
|
||||||
|
features = ["env-filter", "tracing-log"]
|
||||||
|
|
||||||
[dependencies.lettre]
|
[dependencies.lettre]
|
||||||
features = ["builder", "serde", "smtp-transport", "tokio1-rustls-tls"]
|
features = ["builder", "serde", "smtp-transport", "tokio1-rustls-tls"]
|
||||||
@@ -120,16 +126,52 @@ path = "../crates/opaque-handler"
|
|||||||
[dependencies.lldap_validation]
|
[dependencies.lldap_validation]
|
||||||
path = "../crates/validation"
|
path = "../crates/validation"
|
||||||
|
|
||||||
|
[dependencies.opaque-ke]
|
||||||
|
version = "0.7"
|
||||||
|
|
||||||
|
[dependencies.rand]
|
||||||
|
features = ["small_rng", "getrandom"]
|
||||||
|
version = "0.8"
|
||||||
|
|
||||||
|
[dependencies.secstr]
|
||||||
|
features = ["serde"]
|
||||||
|
version = "*"
|
||||||
|
|
||||||
|
[dependencies.serde]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies.strum]
|
||||||
|
features = ["derive"]
|
||||||
|
version = "0.25"
|
||||||
|
|
||||||
|
[dependencies.tokio]
|
||||||
|
features = ["full"]
|
||||||
|
version = "1.25"
|
||||||
|
|
||||||
|
[dependencies.uuid]
|
||||||
|
features = ["v1", "v3", "v4"]
|
||||||
|
version = "1"
|
||||||
|
|
||||||
[dependencies.tracing-forest]
|
[dependencies.tracing-forest]
|
||||||
features = ["smallvec", "chrono", "tokio"]
|
features = ["smallvec", "chrono", "tokio"]
|
||||||
version = "0.3"
|
version = "^0.1.6"
|
||||||
|
|
||||||
[dependencies.actix-tls]
|
[dependencies.actix-tls]
|
||||||
features = ["default", "rustls-0_23"]
|
features = ["default", "rustls-0_23"]
|
||||||
version = "3"
|
version = "3"
|
||||||
|
|
||||||
[dependencies.reqwest]
|
[dependencies.sea-orm]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
features = [
|
||||||
|
"macros",
|
||||||
|
"with-chrono",
|
||||||
|
"with-uuid",
|
||||||
|
"sqlx-all",
|
||||||
|
"runtime-actix-rustls",
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependencies.reqwest]
|
||||||
|
version = "0.11"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["rustls-tls-webpki-roots"]
|
features = ["rustls-tls-webpki-roots"]
|
||||||
|
|
||||||
@@ -138,13 +180,20 @@ version = "2"
|
|||||||
features = ["serde"]
|
features = ["serde"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assert_cmd = "2"
|
assert_cmd = "2.0"
|
||||||
graphql_client = { workspace = true, features = ["graphql_query_derive", "reqwest-rustls"] }
|
mockall = "0.11.4"
|
||||||
ldap3 = { workspace = true }
|
nix = "0.26.2"
|
||||||
mockall = { workspace = true }
|
pretty_assertions = "1"
|
||||||
nix = { version = "0.31", features = ["process", "signal"] }
|
|
||||||
pretty_assertions = { workspace = true }
|
[dev-dependencies.graphql_client]
|
||||||
uuid = { workspace = true }
|
features = ["graphql_query_derive", "reqwest-rustls"]
|
||||||
|
default-features = false
|
||||||
|
version = "0.11"
|
||||||
|
|
||||||
|
[dev-dependencies.ldap3]
|
||||||
|
version = "*"
|
||||||
|
default-features = false
|
||||||
|
features = ["sync", "tls-rustls-ring"]
|
||||||
|
|
||||||
[dev-dependencies.lldap_auth]
|
[dev-dependencies.lldap_auth]
|
||||||
path = "../crates/auth"
|
path = "../crates/auth"
|
||||||
@@ -162,7 +211,7 @@ path = "../crates/sql-backend-handler"
|
|||||||
features = ["test"]
|
features = ["test"]
|
||||||
|
|
||||||
[dev-dependencies.reqwest]
|
[dev-dependencies.reqwest]
|
||||||
workspace = true
|
version = "*"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["json", "blocking", "rustls-tls"]
|
features = ["json", "blocking", "rustls-tls"]
|
||||||
|
|
||||||
@@ -171,6 +220,10 @@ version = "2.0.0"
|
|||||||
default-features = false
|
default-features = false
|
||||||
features = ["file_locks"]
|
features = ["file_locks"]
|
||||||
|
|
||||||
|
[dev-dependencies.uuid]
|
||||||
|
version = "1"
|
||||||
|
features = ["v4"]
|
||||||
|
|
||||||
[dev-dependencies.figment]
|
[dev-dependencies.figment]
|
||||||
features = ["test"]
|
features = ["test"]
|
||||||
version = "0"
|
version = "*"
|
||||||
|
|||||||
+3
-3
@@ -135,7 +135,7 @@ pub struct RunOpts {
|
|||||||
#[clap(long, env = "LLDAP_SERVER_KEY_SEED")]
|
#[clap(long, env = "LLDAP_SERVER_KEY_SEED")]
|
||||||
pub server_key_seed: Option<String>,
|
pub server_key_seed: Option<String>,
|
||||||
|
|
||||||
/// Change ldap host. Default: "::"
|
/// Change ldap host. Default: "0.0.0.0"
|
||||||
#[clap(long, env = "LLDAP_LDAP_HOST")]
|
#[clap(long, env = "LLDAP_LDAP_HOST")]
|
||||||
pub ldap_host: Option<String>,
|
pub ldap_host: Option<String>,
|
||||||
|
|
||||||
@@ -143,7 +143,7 @@ pub struct RunOpts {
|
|||||||
#[clap(long, env = "LLDAP_LDAP_PORT")]
|
#[clap(long, env = "LLDAP_LDAP_PORT")]
|
||||||
pub ldap_port: Option<u16>,
|
pub ldap_port: Option<u16>,
|
||||||
|
|
||||||
/// Change HTTP API host. Default: "::"
|
/// Change HTTP API host. Default: "0.0.0.0"
|
||||||
#[clap(long, env = "LLDAP_HTTP_HOST")]
|
#[clap(long, env = "LLDAP_HTTP_HOST")]
|
||||||
pub http_host: Option<String>,
|
pub http_host: Option<String>,
|
||||||
|
|
||||||
@@ -151,7 +151,7 @@ pub struct RunOpts {
|
|||||||
#[clap(long, env = "LLDAP_HTTP_PORT")]
|
#[clap(long, env = "LLDAP_HTTP_PORT")]
|
||||||
pub http_port: Option<u16>,
|
pub http_port: Option<u16>,
|
||||||
|
|
||||||
/// URL of the server, for password reset links. Default: "http://localhost"
|
/// URL of the server, for password reset links.
|
||||||
#[clap(long, env = "LLDAP_HTTP_URL")]
|
#[clap(long, env = "LLDAP_HTTP_URL")]
|
||||||
pub http_url: Option<Url>,
|
pub http_url: Option<Url>,
|
||||||
|
|
||||||
|
|||||||
@@ -105,11 +105,11 @@ pub struct HttpUrl(pub Url);
|
|||||||
#[derive(Clone, Deserialize, Serialize, derive_builder::Builder, derive_more::Debug)]
|
#[derive(Clone, Deserialize, Serialize, derive_builder::Builder, derive_more::Debug)]
|
||||||
#[builder(pattern = "owned", build_fn(name = "private_build"))]
|
#[builder(pattern = "owned", build_fn(name = "private_build"))]
|
||||||
pub struct Configuration {
|
pub struct Configuration {
|
||||||
#[builder(default = r#"String::from("::")"#)]
|
#[builder(default = r#"String::from("0.0.0.0")"#)]
|
||||||
pub ldap_host: String,
|
pub ldap_host: String,
|
||||||
#[builder(default = "3890")]
|
#[builder(default = "3890")]
|
||||||
pub ldap_port: u16,
|
pub ldap_port: u16,
|
||||||
#[builder(default = r#"String::from("::")"#)]
|
#[builder(default = r#"String::from("0.0.0.0")"#)]
|
||||||
pub http_host: String,
|
pub http_host: String,
|
||||||
#[builder(default = "17170")]
|
#[builder(default = "17170")]
|
||||||
pub http_port: u16,
|
pub http_port: u16,
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ where
|
|||||||
|
|
||||||
/// Actix GraphQL Handler for GET requests
|
/// Actix GraphQL Handler for GET requests
|
||||||
pub async fn get_graphql_handler<Query, Mutation, Subscription, CtxT, S>(
|
pub async fn get_graphql_handler<Query, Mutation, Subscription, CtxT, S>(
|
||||||
schema: &juniper::RootNode<Query, Mutation, Subscription, S>,
|
schema: &juniper::RootNode<'static, Query, Mutation, Subscription, S>,
|
||||||
context: &CtxT,
|
context: &CtxT,
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
) -> Result<HttpResponse, Error>
|
) -> Result<HttpResponse, Error>
|
||||||
@@ -81,7 +81,7 @@ where
|
|||||||
|
|
||||||
/// Actix GraphQL Handler for POST requests
|
/// Actix GraphQL Handler for POST requests
|
||||||
pub async fn post_graphql_handler<Query, Mutation, Subscription, CtxT, S>(
|
pub async fn post_graphql_handler<Query, Mutation, Subscription, CtxT, S>(
|
||||||
schema: &juniper::RootNode<Query, Mutation, Subscription, S>,
|
schema: &juniper::RootNode<'static, Query, Mutation, Subscription, S>,
|
||||||
context: &CtxT,
|
context: &CtxT,
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
mut payload: actix_http::Payload,
|
mut payload: actix_http::Payload,
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ use anyhow::{Context, Result, anyhow};
|
|||||||
use graphql_client::GraphQLQuery;
|
use graphql_client::GraphQLQuery;
|
||||||
use reqwest::blocking::Client;
|
use reqwest::blocking::Client;
|
||||||
|
|
||||||
pub type DateTime = chrono::DateTime<chrono::Utc>;
|
pub type DateTimeUtc = chrono::DateTime<chrono::Utc>;
|
||||||
pub type DateTimeUtc = DateTime;
|
|
||||||
|
|
||||||
#[derive(GraphQLQuery)]
|
#[derive(GraphQLQuery)]
|
||||||
#[graphql(
|
#[graphql(
|
||||||
|
|||||||
+12
-7
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lldap_set_password"
|
name = "lldap_set_password"
|
||||||
version = "0.1.1"
|
version = "0.1.0"
|
||||||
description = "CLI tool to set a user password in LLDAP"
|
description = "CLI tool to set a user password in LLDAP"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
@@ -12,17 +12,22 @@ rust-version.workspace = true
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { workspace = true }
|
anyhow = "*"
|
||||||
rand = { workspace = true }
|
rand = "0.8"
|
||||||
serde = { workspace = true }
|
serde_json = "1"
|
||||||
serde_json = { workspace = true }
|
|
||||||
|
|
||||||
clap = { workspace = true }
|
[dependencies.clap]
|
||||||
|
features = ["std", "color", "suggestions", "derive", "env"]
|
||||||
|
version = "4"
|
||||||
|
|
||||||
[dependencies.lldap_auth]
|
[dependencies.lldap_auth]
|
||||||
path = "../crates/auth"
|
path = "../crates/auth"
|
||||||
features = ["opaque_client"]
|
features = ["opaque_client"]
|
||||||
|
|
||||||
[dependencies.reqwest]
|
[dependencies.reqwest]
|
||||||
workspace = true
|
version = "*"
|
||||||
|
default-features = false
|
||||||
features = ["json", "blocking", "rustls-tls", "rustls-tls-native-roots"]
|
features = ["json", "blocking", "rustls-tls", "rustls-tls-native-roots"]
|
||||||
|
|
||||||
|
[dependencies.serde]
|
||||||
|
workspace = true
|
||||||
|
|||||||
Reference in New Issue
Block a user