Compare commits

..

22 Commits
v0.6.3 ... main

Author SHA1 Message Date
copilot-swe-agent[bot]
dbc85d9594 server: reject LDAP equality filters on list custom attributes with UnwillingToPerform error 2026-05-27 00:06:55 +02:00
Xre0uS
e567d17062 fix: percent-decode user_id route params for users with spaces in ID (#585) 2026-05-26 23:42:08 +02:00
Valentin Tolmer
d293941a44 chore: fmt and clippy 2026-05-26 23:37:26 +02:00
aokblast
203169fe4a server: Bind to :: by default
Healthcheck connects to `localhost`, which may resolve to either IPv4 or
Ipv6 depending on the platform. However, the server was previously bound
to 0.0.0.0, limiting it to IPv4-only.

Switching to :: allows the server to listen on both IPv6 and IPv4 when
the socket is not restricted with IPV6_V6ONLY, as described in RFC 3493.

This improves cross-platform comaptibility and fixes "cargo test"
failures in the healthcheck on FreeBSD.
2026-05-26 12:48:32 +02:00
Matt Van Horn
3bf9ea5206 server: prevent attributes with conflicting types across users/groups (#1426)
Before creating a user attribute, check if a group attribute with
the same name exists with a different type (and vice versa). Return
an error if the types conflict, as LDAP requires each attribute
name to have a single associated type (RFC 4512).

Partial fix for #1202

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-05-26 00:28:34 +02:00
René Neumann
ddd6b469f2 bootstrap: Reintroduce LLDAP_USER_PASSWORD
Ensure no passwords are leaked via cmdline.
2026-05-26 00:05:07 +02:00
Reefsoft
36f10a9947 Fixed a port numbering issue 2026-05-26 00:00:02 +02:00
Reefsoft
fc1731fad6 Added Gogs to example config readme 2026-05-26 00:00:02 +02:00
Reefsoft
f949575a01 Fix style again 2026-05-26 00:00:02 +02:00
Reefsoft
35d0d8005d Fix style issues 2026-05-26 00:00:02 +02:00
Reefsoft
92d1d83282 Fixed the same nitpicks again 2026-05-26 00:00:02 +02:00
Reefsoft
29b2411c6d Fix a port issue and a Bind DN issue 2026-05-26 00:00:02 +02:00
Reefsoft
e96e4c4adf Fixed a problem regarding User DN in SimpleAuth method 2026-05-26 00:00:02 +02:00
Reefsoft
2cd33c7215 Apply patches proposed by CodeRabbit 2026-05-26 00:00:02 +02:00
Reefsoft
dfe0773549 Add Simple Auth to Gogs example config 2026-05-26 00:00:02 +02:00
Tim Beermann
52a08d3ad9 doc: added LLDAP_HTTP_URL env variable for SMTP settings
Signed-off-by: Tim Beermann <tibeer@berryit.de>
2026-05-25 23:57:40 +02:00
Valentin Tolmer
2dc6178bd0 chore: bump MSRV to 1.91
Otherwise cargo install wasm-pack doesn't work
2026-05-25 23:45:46 +02:00
Kieran
ed7484bffb Added more details to licence warning 2026-05-25 23:45:14 +02:00
Kieran
68fc426ba3 Added example configuration for Elasticsearch + Kibanna 2026-05-25 23:45:14 +02:00
Glujaz
a3d4eb04be Update udm_identity_end_point.md
Replaced Member by uniqueMember in Member Attribute. Otherwise the groups are not detected, and the rules are not applied.
2026-05-25 23:41:32 +02:00
VisableSampling
dc883a060a Update Alpine release image to 3.22 2026-05-05 02:39:41 +02:00
Valentin Tolmer
82b16a3716 changelog: fix security advisory description 2026-05-01 00:51:22 +02:00
22 changed files with 371 additions and 60 deletions

View File

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

View File

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

View File

@@ -24,7 +24,7 @@ on:
env:
CARGO_TERM_COLOR: always
MSRV: "1.89.0"
MSRV: "1.91.0"
### CI Docs

View File

@@ -8,7 +8,7 @@ on:
env:
CARGO_TERM_COLOR: always
MSRV: "1.89.0"
MSRV: "1.91.0"
jobs:
pre_job:

View File

@@ -34,7 +34,7 @@ Small release, focused on LDAP compatibility, TLS maintenance, dependency upgrad
- Updated the LDAP dependency stack, including `ldap3_proto`, in response to
security advisory
[`GHSA-qcxq-75wr-5cm8`](https://github.com/kanidm/ldap3/security/advisories/GHSA-qcxq-75wr-5cm8),
where a specially crafted LDAP query could make the server use unbounded RAM
where a specially crafted LDAP query could make the server crash
### Cleanups

1
Cargo.lock generated
View File

@@ -2743,6 +2743,7 @@ dependencies = [
"lldap_auth",
"lldap_frontend_options",
"lldap_validation",
"percent-encoding",
"rand 0.8.6",
"serde",
"serde_json",

View File

@@ -16,7 +16,7 @@ edition = "2024"
homepage = "https://github.com/lldap/lldap"
license = "GPL-3.0-only"
repository = "https://github.com/lldap/lldap"
rust-version = "1.89.0"
rust-version = "1.91.0"
[profile.release]
lto = true

View File

@@ -24,6 +24,7 @@ rand = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
strum = { workspace = true }
percent-encoding = "2"
url-escape = "0.1.1"
validator = "0.14"
validator_derive = "0.14"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,11 +19,13 @@ configuration files:
- [Dolibarr](dolibarr.md)
- [Duo Auth Proxy](duo_auth_proxy.md)
- [Ejabberd](ejabberd.md)
- [Elasticsearch](elasticsearch.md)
- [Emby](emby.md)
- [Ergo IRCd](ergo.md)
- [Gerrit](gerrit.md)
- [Gitea](gitea.md)
- [GitLab](gitlab.md)
- [Gogs](gogs.md)
- [Grafana](grafana_ldap_config.toml)
- [Grocy](grocy.md)
- [Harbor](harbor.md)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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