add declarative lldap using secrets contract
This commit is contained in:
parent
94a5b7710b
commit
b50f45e17e
8 changed files with 1260 additions and 9 deletions
|
|
@ -311,8 +311,11 @@
|
|||
"blocks-lldap-global-setup": [
|
||||
"blocks-lldap.html#blocks-lldap-global-setup"
|
||||
],
|
||||
"blocks-lldap-manage-user-group": [
|
||||
"blocks-lldap.html#blocks-lldap-manage-user-group"
|
||||
"blocks-lldap-manage-groups": [
|
||||
"blocks-lldap.html#blocks-lldap-manage-groups"
|
||||
],
|
||||
"blocks-lldap-manage-users": [
|
||||
"blocks-lldap.html#blocks-lldap-manage-users"
|
||||
],
|
||||
"blocks-lldap-options": [
|
||||
"blocks-lldap.html#blocks-lldap-options"
|
||||
|
|
@ -362,6 +365,105 @@
|
|||
"blocks-lldap-options-shb.lldap.enable": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.enable"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.ensureGroupFields": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.ensureGroupFields"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.ensureGroupFields._name_.attributeType": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.ensureGroupFields._name_.attributeType"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.ensureGroupFields._name_.isEditable": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.ensureGroupFields._name_.isEditable"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.ensureGroupFields._name_.isList": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.ensureGroupFields._name_.isList"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.ensureGroupFields._name_.isVisible": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.ensureGroupFields._name_.isVisible"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.ensureGroupFields._name_.name": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.ensureGroupFields._name_.name"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.ensureGroups": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.ensureGroups"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.ensureGroups._name_.name": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.ensureGroups._name_.name"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.ensureUserFields": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.ensureUserFields"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.ensureUserFields._name_.attributeType": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.ensureUserFields._name_.attributeType"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.ensureUserFields._name_.isEditable": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.ensureUserFields._name_.isEditable"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.ensureUserFields._name_.isList": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.ensureUserFields._name_.isList"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.ensureUserFields._name_.isVisible": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.ensureUserFields._name_.isVisible"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.ensureUserFields._name_.name": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.ensureUserFields._name_.name"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.ensureUsers": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.ensureUsers"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.ensureUsers._name_.avatar_file": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.ensureUsers._name_.avatar_file"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.ensureUsers._name_.avatar_url": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.ensureUsers._name_.avatar_url"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.ensureUsers._name_.displayName": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.ensureUsers._name_.displayName"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.ensureUsers._name_.email": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.ensureUsers._name_.email"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.ensureUsers._name_.firstName": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.ensureUsers._name_.firstName"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.ensureUsers._name_.gravatar_avatar": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.ensureUsers._name_.gravatar_avatar"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.ensureUsers._name_.groups": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.ensureUsers._name_.groups"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.ensureUsers._name_.id": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.ensureUsers._name_.id"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.ensureUsers._name_.lastName": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.ensureUsers._name_.lastName"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.ensureUsers._name_.password": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.ensureUsers._name_.password"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.ensureUsers._name_.password.request": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.ensureUsers._name_.password.request"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.ensureUsers._name_.password.request.group": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.ensureUsers._name_.password.request.group"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.ensureUsers._name_.password.request.mode": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.ensureUsers._name_.password.request.mode"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.ensureUsers._name_.password.request.owner": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.ensureUsers._name_.password.request.owner"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.ensureUsers._name_.password.request.restartUnits": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.ensureUsers._name_.password.request.restartUnits"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.ensureUsers._name_.password.result": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.ensureUsers._name_.password.result"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.ensureUsers._name_.password.result.path": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.ensureUsers._name_.password.result.path"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.ensureUsers._name_.weser_avatar": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.ensureUsers._name_.weser_avatar"
|
||||
],
|
||||
"blocks-lldap-options-shb.lldap.jwtSecret": [
|
||||
"blocks-lldap.html#blocks-lldap-options-shb.lldap.jwtSecret"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -15,6 +15,11 @@
|
|||
let
|
||||
originPkgs = nixpkgs.legacyPackages.${system};
|
||||
shbPatches = originPkgs.lib.optionals (system == "x86_64-linux") [
|
||||
# Get rid of lldap patches when https://github.com/NixOS/nixpkgs/pull/425923 is merged.
|
||||
./patches/0001-lldap-lldap-0.6.1-unstable-2025-07-16.patch
|
||||
./patches/0002-lldap-add-options-to-set-important-secrets.patch
|
||||
./patches/0003-lldap-bootstrap-init-unstable-2025-07-16-lldap-add-e.patch
|
||||
|
||||
# Leaving commented out as an example.
|
||||
# (originPkgs.fetchpatch {
|
||||
# url = "https://github.com/NixOS/nixpkgs/pull/317107.patch";
|
||||
|
|
|
|||
|
|
@ -6,6 +6,46 @@ let
|
|||
contracts = pkgs.callPackage ../contracts {};
|
||||
|
||||
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
||||
|
||||
inherit (lib) mkOption types;
|
||||
|
||||
ensureFormat = pkgs.formats.json { };
|
||||
|
||||
ensureFieldsOptions = name: {
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
description = "Name of the field.";
|
||||
default = name;
|
||||
};
|
||||
|
||||
attributeType = mkOption {
|
||||
type = types.enum [
|
||||
"STRING"
|
||||
"INTEGER"
|
||||
"JPEG"
|
||||
"DATE_TIME"
|
||||
];
|
||||
description = "Attribute type.";
|
||||
};
|
||||
|
||||
isEditable = mkOption {
|
||||
type = types.bool;
|
||||
description = "Is field editable.";
|
||||
default = true;
|
||||
};
|
||||
|
||||
isList = mkOption {
|
||||
type = types.bool;
|
||||
description = "Is field a list.";
|
||||
default = false;
|
||||
};
|
||||
|
||||
isVisible = mkOption {
|
||||
type = types.bool;
|
||||
description = "Is field visible in UI.";
|
||||
default = true;
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
options.shb.lldap = {
|
||||
|
|
@ -118,10 +158,155 @@ in
|
|||
};
|
||||
};
|
||||
};
|
||||
|
||||
ensureUsers = mkOption {
|
||||
description = ''
|
||||
Create the users defined here on service startup.
|
||||
|
||||
If `enforceEnsure` option is `true`, the groups
|
||||
users belong to must be present in the `ensureGroups` option.
|
||||
|
||||
Non-default options must be added to the `ensureGroupFields` option.
|
||||
'';
|
||||
default = { };
|
||||
type = types.attrsOf (
|
||||
types.submodule (
|
||||
{ name, ... }:
|
||||
{
|
||||
freeformType = ensureFormat.type;
|
||||
|
||||
options = {
|
||||
id = mkOption {
|
||||
type = types.str;
|
||||
description = "Username.";
|
||||
default = name;
|
||||
};
|
||||
|
||||
email = mkOption {
|
||||
type = types.str;
|
||||
description = "Email.";
|
||||
};
|
||||
|
||||
password = mkOption {
|
||||
description = "Password.";
|
||||
type = lib.types.submodule {
|
||||
options = contracts.secret.mkRequester {
|
||||
mode = "0440";
|
||||
owner = "lldap";
|
||||
group = "lldap";
|
||||
restartUnits = [ "lldap.service" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
displayName = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Display name.";
|
||||
};
|
||||
|
||||
firstName = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "First name.";
|
||||
};
|
||||
|
||||
lastName = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Last name.";
|
||||
};
|
||||
|
||||
avatar_file = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Avatar file. Must be a valid path to jpeg file (ignored if avatar_url specified)";
|
||||
};
|
||||
|
||||
avatar_url = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Avatar url. must be a valid URL to jpeg file (ignored if gravatar_avatar specified)";
|
||||
};
|
||||
|
||||
gravatar_avatar = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Get avatar from Gravatar using the email.";
|
||||
};
|
||||
|
||||
weser_avatar = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Convert avatar retrieved by gravatar or the URL.";
|
||||
};
|
||||
|
||||
groups = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
description = "Groups the user would be a member of (all the groups must be specified in group config files).";
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
ensureGroups = mkOption {
|
||||
description = ''
|
||||
Create the groups defined here on service startup.
|
||||
|
||||
Non-default options must be added to the `ensureGroupFields` option.
|
||||
'';
|
||||
default = { };
|
||||
type = types.attrsOf (
|
||||
types.submodule (
|
||||
{ name, ... }:
|
||||
{
|
||||
freeformType = ensureFormat.type;
|
||||
|
||||
options = {
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
description = "Name of the group.";
|
||||
default = name;
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
ensureUserFields = mkOption {
|
||||
description = "Extra fields for users";
|
||||
default = { };
|
||||
type = types.attrsOf (
|
||||
types.submodule (
|
||||
{ name, ... }:
|
||||
{
|
||||
options = ensureFieldsOptions name;
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
ensureGroupFields = mkOption {
|
||||
description = "Extra fields for groups";
|
||||
default = { };
|
||||
type = types.attrsOf (
|
||||
types.submodule (
|
||||
{ name, ... }:
|
||||
{
|
||||
options = ensureFieldsOptions name;
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
|
||||
|
|
@ -151,10 +336,12 @@ in
|
|||
services.lldap = {
|
||||
enable = true;
|
||||
|
||||
environment = {
|
||||
LLDAP_JWT_SECRET_FILE = toString cfg.jwtSecret.result.path;
|
||||
LLDAP_LDAP_USER_PASS_FILE = toString cfg.ldapUserPassword.result.path;
|
||||
adminPasswordFile = toString cfg.ldapUserPassword.result.path;
|
||||
jwtSecretFile = toString cfg.jwtSecret.result.path;
|
||||
resetAdminPassword = "always";
|
||||
enforceEnsure = true;
|
||||
|
||||
environment = {
|
||||
RUST_LOG = lib.mkIf cfg.debug "debug";
|
||||
};
|
||||
|
||||
|
|
@ -170,6 +357,11 @@ in
|
|||
|
||||
verbose = cfg.debug;
|
||||
};
|
||||
|
||||
inherit (cfg) ensureGroups ensureUserFields ensureGroupFields;
|
||||
ensureUsers = lib.mapAttrs (n: v: (lib.removeAttrs v [ "password" ]) // {
|
||||
"password_file" = v.password.result.path;
|
||||
}) cfg.ensureUsers;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,14 +53,94 @@ by adding the following line:
|
|||
shb.lldap.restrictAccessIPRange = "192.168.50.0/24";
|
||||
```
|
||||
|
||||
## Manage User and Group {#blocks-lldap-manage-user-group}
|
||||
## Manage Groups {#blocks-lldap-manage-groups}
|
||||
|
||||
Currently, managing users and groups must be done manually.
|
||||
Work is in progress to make this declarative.
|
||||
The following snippet will create group named "family" if it does not exist yet.
|
||||
Also, all other groups will be deleted and only the "family" group will remain.
|
||||
|
||||
Note that the `lldap_admin`, `lldap_password_manager` and `lldap_strict_readonly` groups, which are internal to LLDAP, will always exist.
|
||||
|
||||
```nix
|
||||
{
|
||||
shb.lldap.ensureGroups = {
|
||||
family = {};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Changing the configuration to the following will add a new group "friends":
|
||||
|
||||
```nix
|
||||
{
|
||||
shb.lldap.ensureGroups = {
|
||||
family = {};
|
||||
friends = {};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Switching back the configuration to the previous one will delete the group "friends":
|
||||
|
||||
```nix
|
||||
{
|
||||
shb.lldap.ensureGroups = {
|
||||
family = {};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Custom fields can be added to groups as long as they are added to the `ensureGroupFields` field:
|
||||
|
||||
```nix
|
||||
shb.lldap = {
|
||||
ensureGroupFields = {
|
||||
mygroupattribute = {
|
||||
attributeType = "STRING";
|
||||
};
|
||||
};
|
||||
|
||||
ensureGroups = {
|
||||
family = {
|
||||
mygroupattribute = "Managed by NixOS";
|
||||
};
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## Manage Users {#blocks-lldap-manage-users}
|
||||
|
||||
The following snippet creates a user and makes it a member of the "family" group.
|
||||
|
||||
```nix
|
||||
{
|
||||
shb.lldap.ensureUsers = {
|
||||
dad = {
|
||||
email = "dad@example.com";
|
||||
displayName = "Dad";
|
||||
firstName = "First Name";
|
||||
lastName = "Last Name";
|
||||
groups = [ "family" ];
|
||||
password.result = config.shb.sops.secret."dad".result;
|
||||
};
|
||||
};
|
||||
|
||||
shb.sops.secret."dad".request =
|
||||
shb.ldap.ensureUsers.dad.password.request;
|
||||
}
|
||||
```
|
||||
|
||||
The password field assumes usage of the [sops block][] to provide secrets
|
||||
although any blocks providing the [secrets contract][] works too.
|
||||
|
||||
[sops block]: blocks-sops.html
|
||||
[secrets contract]: contracts-secrets.html
|
||||
|
||||
The user is still editable through the UI.
|
||||
That being said, any change will be overwritten next time the configuration is applied.
|
||||
|
||||
## Troubleshooting {#blocks-lldap-troubleshooting}
|
||||
|
||||
To increase logging verbosity, add:
|
||||
To increase logging verbosity and see the trace of the GraphQL queries, add:
|
||||
|
||||
```nix
|
||||
shb.lldap.debug = true;
|
||||
|
|
@ -69,6 +149,8 @@ shb.lldap.debug = true;
|
|||
Note that verbosity is truly verbose here
|
||||
so you will want to revert this at some point.
|
||||
|
||||
To see the logs, then run `journalctl -u lldap.service`.
|
||||
|
||||
## Tests {#blocks-lldap-tests}
|
||||
|
||||
Specific integration tests are defined in [`/test/blocks/lldap.nix`](@REPO@/test/blocks/lldap.nix).
|
||||
|
|
|
|||
163
patches/0001-lldap-lldap-0.6.1-unstable-2025-07-16.patch
Normal file
163
patches/0001-lldap-lldap-0.6.1-unstable-2025-07-16.patch
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
From 5bc3588204e71a77d7e0bfccc4cb70e6ccfbb54b Mon Sep 17 00:00:00 2001
|
||||
From: ibizaman <ibizaman@tiserbox.com>
|
||||
Date: Tue, 15 Jul 2025 18:42:42 +0200
|
||||
Subject: [PATCH 1/3] lldap: lldap 0.6.1 -> unstable-2025-07-16
|
||||
|
||||
---
|
||||
.../0001-parameterize-frontend-location.patch | 64 -------------------
|
||||
pkgs/by-name/ll/lldap/package.nix | 30 +++++----
|
||||
2 files changed, 16 insertions(+), 78 deletions(-)
|
||||
delete mode 100644 pkgs/by-name/ll/lldap/0001-parameterize-frontend-location.patch
|
||||
|
||||
diff --git a/pkgs/by-name/ll/lldap/0001-parameterize-frontend-location.patch b/pkgs/by-name/ll/lldap/0001-parameterize-frontend-location.patch
|
||||
deleted file mode 100644
|
||||
index c33f5a7afa10..000000000000
|
||||
--- a/pkgs/by-name/ll/lldap/0001-parameterize-frontend-location.patch
|
||||
+++ /dev/null
|
||||
@@ -1,64 +0,0 @@
|
||||
-From a09babb0cd9dd532ad2de920a2a35aa03d740dc6 Mon Sep 17 00:00:00 2001
|
||||
-From: Herwig Hochleitner <herwig@bendlas.net>
|
||||
-Date: Thu, 8 Aug 2024 00:29:14 +0200
|
||||
-Subject: [PATCH] parameterize frontend location
|
||||
-
|
||||
----
|
||||
- server/src/infra/tcp_server.rs | 14 +++++++-------
|
||||
- 1 file changed, 7 insertions(+), 7 deletions(-)
|
||||
-
|
||||
-diff --git a/server/src/infra/tcp_server.rs b/server/src/infra/tcp_server.rs
|
||||
-index fa5f11f..16e64c5 100644
|
||||
---- a/server/src/infra/tcp_server.rs
|
||||
-+++ b/server/src/infra/tcp_server.rs
|
||||
-@@ -25,7 +25,7 @@ use std::sync::RwLock;
|
||||
- use tracing::info;
|
||||
-
|
||||
- async fn index<Backend>(data: web::Data<AppState<Backend>>) -> actix_web::Result<impl Responder> {
|
||||
-- let mut file = std::fs::read_to_string(r"./app/index.html")?;
|
||||
-+ let mut file = std::fs::read_to_string(r"@frontend@/index.html")?;
|
||||
-
|
||||
- if data.server_url.path() != "/" {
|
||||
- file = file.replace(
|
||||
-@@ -80,7 +80,7 @@ pub(crate) fn error_to_http_response(error: TcpError) -> HttpResponse {
|
||||
- async fn main_js_handler<Backend>(
|
||||
- data: web::Data<AppState<Backend>>,
|
||||
- ) -> actix_web::Result<impl Responder> {
|
||||
-- let mut file = std::fs::read_to_string(r"./app/static/main.js")?;
|
||||
-+ let mut file = std::fs::read_to_string(r"@frontend@/static/main.js")?;
|
||||
-
|
||||
- if data.server_url.path() != "/" {
|
||||
- file = file.replace("/pkg/", format!("{}/pkg/", data.server_url.path()).as_str());
|
||||
-@@ -92,12 +92,12 @@ async fn main_js_handler<Backend>(
|
||||
- }
|
||||
-
|
||||
- async fn wasm_handler() -> actix_web::Result<impl Responder> {
|
||||
-- Ok(actix_files::NamedFile::open_async("./app/pkg/lldap_app_bg.wasm").await?)
|
||||
-+ Ok(actix_files::NamedFile::open_async("@frontend@/pkg/lldap_app_bg.wasm").await?)
|
||||
- }
|
||||
-
|
||||
- async fn wasm_handler_compressed() -> actix_web::Result<impl Responder> {
|
||||
- Ok(
|
||||
-- actix_files::NamedFile::open_async("./app/pkg/lldap_app_bg.wasm.gz")
|
||||
-+ actix_files::NamedFile::open_async("@frontend@/pkg/lldap_app_bg.wasm.gz")
|
||||
- .await?
|
||||
- .customize()
|
||||
- .insert_header(header::ContentEncoding::Gzip)
|
||||
-@@ -143,11 +143,11 @@ fn http_config<Backend>(
|
||||
- .service(web::resource("/pkg/lldap_app_bg.wasm").route(web::route().to(wasm_handler)))
|
||||
- .service(web::resource("/static/main.js").route(web::route().to(main_js_handler::<Backend>)))
|
||||
- // Serve the /pkg path with the compiled WASM app.
|
||||
-- .service(Files::new("/pkg", "./app/pkg"))
|
||||
-+ .service(Files::new("/pkg", "@frontend@/pkg"))
|
||||
- // Serve static files
|
||||
-- .service(Files::new("/static", "./app/static"))
|
||||
-+ .service(Files::new("/static", "@frontend@/static"))
|
||||
- // Serve static fonts
|
||||
-- .service(Files::new("/static/fonts", "./app/static/fonts"))
|
||||
-+ .service(Files::new("/static/fonts", "@frontend@/static/fonts"))
|
||||
- // Default to serve index.html for unknown routes, to support routing.
|
||||
- .default_service(web::route().guard(guard::Get()).to(index::<Backend>));
|
||||
- }
|
||||
---
|
||||
-2.45.2
|
||||
-
|
||||
diff --git a/pkgs/by-name/ll/lldap/package.nix b/pkgs/by-name/ll/lldap/package.nix
|
||||
index 6931256080b2..41e4a332018e 100644
|
||||
--- a/pkgs/by-name/ll/lldap/package.nix
|
||||
+++ b/pkgs/by-name/ll/lldap/package.nix
|
||||
@@ -3,29 +3,30 @@
|
||||
fetchFromGitHub,
|
||||
lib,
|
||||
lldap,
|
||||
+ makeWrapper,
|
||||
nixosTests,
|
||||
rustPlatform,
|
||||
rustc,
|
||||
- wasm-bindgen-cli_0_2_95,
|
||||
+ wasm-bindgen-cli_0_2_100,
|
||||
wasm-pack,
|
||||
which,
|
||||
}:
|
||||
|
||||
let
|
||||
|
||||
- commonDerivationAttrs = rec {
|
||||
+ commonDerivationAttrs = {
|
||||
pname = "lldap";
|
||||
- version = "0.6.1";
|
||||
+ version = "unstable-2025-07-16";
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "lldap";
|
||||
repo = "lldap";
|
||||
- rev = "v${version}";
|
||||
- hash = "sha256-iQ+Vv9kx/pWHoa/WZChBK+FD2r1avzWWz57bnnzRjUg=";
|
||||
+ rev = "78337bce722c3573d9fc6eafe345a3dbce4b9119";
|
||||
+ hash = "sha256-/djLboAQwK/KQ0u9vzoOdDHwh/BQSvMa8lQkABn10Cw=";
|
||||
};
|
||||
|
||||
useFetchCargoVendor = true;
|
||||
- cargoHash = "sha256-qXYgr9uRswuo9hwVROUX9KUKpkzR0VEcXImbdyOgxsY=";
|
||||
+ cargoHash = "sha256-/dyrtX2FUHSGkJ6AkCM81iPqI03IWA0tecR4KHSx8gA=";
|
||||
|
||||
};
|
||||
|
||||
@@ -36,7 +37,7 @@ let
|
||||
|
||||
nativeBuildInputs = [
|
||||
wasm-pack
|
||||
- wasm-bindgen-cli_0_2_95
|
||||
+ wasm-bindgen-cli_0_2_100
|
||||
binaryen
|
||||
which
|
||||
rustc
|
||||
@@ -69,12 +70,10 @@ rustPlatform.buildRustPackage (
|
||||
"lldap_set_password"
|
||||
];
|
||||
|
||||
- patches = [
|
||||
- ./0001-parameterize-frontend-location.patch
|
||||
- ];
|
||||
-
|
||||
- postPatch = ''
|
||||
- substituteInPlace server/src/infra/tcp_server.rs --subst-var-by frontend '${frontend}'
|
||||
+ nativeBuildInputs = [ makeWrapper ];
|
||||
+ postInstall = ''
|
||||
+ wrapProgram $out/bin/lldap \
|
||||
+ --set LLDAP_ASSETS_PATH ${frontend}
|
||||
'';
|
||||
|
||||
passthru = {
|
||||
@@ -90,7 +89,10 @@ rustPlatform.buildRustPackage (
|
||||
changelog = "https://github.com/lldap/lldap/blob/v${lldap.version}/CHANGELOG.md";
|
||||
license = licenses.gpl3Only;
|
||||
platforms = platforms.linux;
|
||||
- maintainers = with maintainers; [ bendlas ];
|
||||
+ maintainers = with maintainers; [
|
||||
+ bendlas
|
||||
+ ibizaman
|
||||
+ ];
|
||||
mainProgram = "lldap";
|
||||
};
|
||||
}
|
||||
--
|
||||
2.49.0
|
||||
|
||||
133
patches/0002-lldap-add-options-to-set-important-secrets.patch
Normal file
133
patches/0002-lldap-add-options-to-set-important-secrets.patch
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
From f898e786bfb07399abf4e8173ee525c57eb41984 Mon Sep 17 00:00:00 2001
|
||||
From: ibizaman <ibizaman@tiserbox.com>
|
||||
Date: Wed, 16 Jul 2025 03:04:44 +0200
|
||||
Subject: [PATCH 2/3] lldap: add options to set important secrets
|
||||
|
||||
---
|
||||
nixos/modules/services/databases/lldap.nix | 64 ++++++++++++++++++++++
|
||||
nixos/tests/lldap.nix | 16 +++++-
|
||||
2 files changed, 77 insertions(+), 3 deletions(-)
|
||||
|
||||
diff --git a/nixos/modules/services/databases/lldap.nix b/nixos/modules/services/databases/lldap.nix
|
||||
index a9fbe8f7e11a..518b39ba7a86 100644
|
||||
--- a/nixos/modules/services/databases/lldap.nix
|
||||
+++ b/nixos/modules/services/databases/lldap.nix
|
||||
@@ -37,6 +37,47 @@ in
|
||||
'';
|
||||
};
|
||||
|
||||
+ adminPasswordFile = mkOption {
|
||||
+ type = types.nullOr types.str;
|
||||
+ default = null;
|
||||
+ description = ''
|
||||
+ Path to a file containing the default admin password.
|
||||
+ '';
|
||||
+ };
|
||||
+
|
||||
+ resetAdminPassword = mkOption {
|
||||
+ type = types.nullOr (
|
||||
+ types.oneOf [
|
||||
+ types.bool
|
||||
+ (types.enum [ "always" ])
|
||||
+ ]
|
||||
+ );
|
||||
+ default = false;
|
||||
+ description = ''
|
||||
+ Force reset of the admin password.
|
||||
+
|
||||
+ Break glass in case of emergency: if you lost the admin password, you
|
||||
+ can set this to true to force a reset of the admin password to the value
|
||||
+ of `adminPasswordFile`.
|
||||
+
|
||||
+ Alternatively, you can set it to `"always"` to reset every time the server starts
|
||||
+ which makes for a more declarative configuration.
|
||||
+
|
||||
+ The difference between `true` and `"always"` is the former is intended for a one time fix
|
||||
+ while the latter is intended for a declarative workflow. In practice, the result
|
||||
+ is the same: the password gets reset. The only practical difference is the former
|
||||
+ outputs a warning message while the latter outputs an info message.
|
||||
+ '';
|
||||
+ };
|
||||
+
|
||||
+ jwtSecretFile = mkOption {
|
||||
+ type = types.nullOr types.str;
|
||||
+ default = null;
|
||||
+ description = ''
|
||||
+ Path to a file containing the default admin password.
|
||||
+ '';
|
||||
+ };
|
||||
+
|
||||
settings = mkOption {
|
||||
description = ''
|
||||
Free-form settings written directly to the `lldap_config.toml` file.
|
||||
@@ -108,6 +149,29 @@ in
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
+ assertions = [
|
||||
+ {
|
||||
+ assertion = cfg.adminPasswordFile == null || cfg.resetAdminPassword != false;
|
||||
+ message = ''
|
||||
+ The default admin password is set declaratively with `adminPasswordFile` option but the `resetAdminPassword` is set to `false`.
|
||||
+ This means the admin password can be changed through the UI and will drift from the one defined in your nix config.
|
||||
+ Please set the `resetAdminPassword` option to `true` or `"always"`.
|
||||
+ '';
|
||||
+ }
|
||||
+ ];
|
||||
+
|
||||
+ services.lldap.environment = {
|
||||
+ LLDAP_JWT_SECRET_FILE = lib.mkIf (cfg.jwtSecretFile != null) cfg.jwtSecretFile;
|
||||
+ LLDAP_LDAP_USER_PASS_FILE = lib.mkIf (cfg.adminPasswordFile != null) cfg.adminPasswordFile;
|
||||
+ LLDAP_FORCE_LDAP_USER_PASS_RESET =
|
||||
+ if builtins.isString cfg.resetAdminPassword then
|
||||
+ cfg.resetAdminPassword
|
||||
+ else if cfg.resetAdminPassword then
|
||||
+ "true"
|
||||
+ else
|
||||
+ "false";
|
||||
+ };
|
||||
+
|
||||
systemd.services.lldap = {
|
||||
description = "Lightweight LDAP server (lldap)";
|
||||
wants = [ "network-online.target" ];
|
||||
diff --git a/nixos/tests/lldap.nix b/nixos/tests/lldap.nix
|
||||
index c2e48525a5f3..e88fa37ab83d 100644
|
||||
--- a/nixos/tests/lldap.nix
|
||||
+++ b/nixos/tests/lldap.nix
|
||||
@@ -1,4 +1,7 @@
|
||||
{ ... }:
|
||||
+let
|
||||
+ adminPassword = "mySecretPassword";
|
||||
+in
|
||||
{
|
||||
name = "lldap";
|
||||
|
||||
@@ -7,6 +10,11 @@
|
||||
{
|
||||
services.lldap = {
|
||||
enable = true;
|
||||
+
|
||||
+ adminPasswordFile = toString (pkgs.writeText "adminPasswordFile" adminPassword);
|
||||
+ resetAdminPassword = "always";
|
||||
+ enforceEnsure = true;
|
||||
+
|
||||
settings = {
|
||||
verbose = true;
|
||||
ldap_base_dn = "dc=example,dc=com";
|
||||
@@ -22,8 +30,10 @@
|
||||
|
||||
machine.succeed("curl --location --fail http://localhost:17170/")
|
||||
|
||||
- print(
|
||||
- machine.succeed('ldapsearch -H ldap://localhost:3890 -D uid=admin,ou=people,dc=example,dc=com -b "ou=people,dc=example,dc=com" -w password')
|
||||
- )
|
||||
+ response = machine.fail('ldapsearch -H ldap://localhost:3890 -D uid=admin,ou=people,dc=example,dc=com -b "ou=people,dc=example,dc=com" -w password')
|
||||
+ print(response)
|
||||
+
|
||||
+ response = machine.succeed('ldapsearch -H ldap://localhost:3890 -D uid=admin,ou=people,dc=example,dc=com -b "ou=people,dc=example,dc=com" -w ${adminPassword}')
|
||||
+ print(response)
|
||||
'';
|
||||
}
|
||||
--
|
||||
2.49.0
|
||||
|
||||
|
|
@ -0,0 +1,549 @@
|
|||
From 75b9437558a22eabff74339569f98b567f0a0d04 Mon Sep 17 00:00:00 2001
|
||||
From: ibizaman <ibizaman@tiserbox.com>
|
||||
Date: Wed, 16 Jul 2025 17:51:57 +0200
|
||||
Subject: [PATCH 3/3] lldap-bootstrap: init unstable-2025-07-17, lldap: add
|
||||
ensure options
|
||||
|
||||
---
|
||||
nixos/modules/services/databases/lldap.nix | 282 +++++++++++++++++++-
|
||||
nixos/tests/lldap.nix | 116 +++++++-
|
||||
pkgs/by-name/ll/lldap-bootstrap/package.nix | 54 ++++
|
||||
3 files changed, 446 insertions(+), 6 deletions(-)
|
||||
create mode 100644 pkgs/by-name/ll/lldap-bootstrap/package.nix
|
||||
|
||||
diff --git a/nixos/modules/services/databases/lldap.nix b/nixos/modules/services/databases/lldap.nix
|
||||
index 518b39ba7a86..b7c4c85f751f 100644
|
||||
--- a/nixos/modules/services/databases/lldap.nix
|
||||
+++ b/nixos/modules/services/databases/lldap.nix
|
||||
@@ -2,13 +2,82 @@
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
- utils,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
cfg = config.services.lldap;
|
||||
format = pkgs.formats.toml { };
|
||||
+
|
||||
+ inherit (lib) mkOption types;
|
||||
+
|
||||
+ ensureFormat = pkgs.formats.json { };
|
||||
+ ensureGenerate =
|
||||
+ name: source: ensureFormat.generate name (lib.filterAttrsRecursive (n: v: v != null) source);
|
||||
+
|
||||
+ ensureFieldsOptions = name: {
|
||||
+ name = mkOption {
|
||||
+ type = types.str;
|
||||
+ description = "Name of the field.";
|
||||
+ default = name;
|
||||
+ };
|
||||
+
|
||||
+ attributeType = mkOption {
|
||||
+ type = types.enum [
|
||||
+ "STRING"
|
||||
+ "INTEGER"
|
||||
+ "JPEG"
|
||||
+ "DATE_TIME"
|
||||
+ ];
|
||||
+ description = "Attribute type.";
|
||||
+ };
|
||||
+
|
||||
+ isEditable = mkOption {
|
||||
+ type = types.bool;
|
||||
+ description = "Is field editable.";
|
||||
+ default = true;
|
||||
+ };
|
||||
+
|
||||
+ isList = mkOption {
|
||||
+ type = types.bool;
|
||||
+ description = "Is field a list.";
|
||||
+ default = false;
|
||||
+ };
|
||||
+
|
||||
+ isVisible = mkOption {
|
||||
+ type = types.bool;
|
||||
+ description = "Is field visible in UI.";
|
||||
+ default = true;
|
||||
+ };
|
||||
+ };
|
||||
+
|
||||
+ allUserGroups = lib.flatten (lib.mapAttrsToList (n: u: u.groups) cfg.ensureUsers);
|
||||
+ # The three hardcoded groups are always created when the service starts.
|
||||
+ allGroups = lib.mapAttrsToList (n: g: g.name) cfg.ensureGroups ++ [
|
||||
+ "lldap_admin"
|
||||
+ "lldap_password_manager"
|
||||
+ "lldap_strict_readonly"
|
||||
+ ];
|
||||
+ userGroupNotInEnsuredGroup = lib.sortOn lib.id (
|
||||
+ lib.unique (lib.subtractLists allGroups allUserGroups)
|
||||
+ );
|
||||
+ someUsersBelongToNonEnsuredGroup = (lib.lists.length userGroupNotInEnsuredGroup) > 0;
|
||||
+
|
||||
+ generateEnsureConfigDir =
|
||||
+ name: source:
|
||||
+ let
|
||||
+ genOne =
|
||||
+ name: sourceOne:
|
||||
+ pkgs.writeTextDir "configs/${name}.json" (
|
||||
+ builtins.readFile (ensureGenerate "configs/${name}.json" sourceOne)
|
||||
+ );
|
||||
+ in
|
||||
+ "${
|
||||
+ pkgs.symlinkJoin {
|
||||
+ inherit name;
|
||||
+ paths = lib.mapAttrsToList genOne source;
|
||||
+ }
|
||||
+ }/configs";
|
||||
in
|
||||
{
|
||||
options.services.lldap = with lib; {
|
||||
@@ -16,6 +85,8 @@ in
|
||||
|
||||
package = mkPackageOption pkgs "lldap" { };
|
||||
|
||||
+ bootstrap-package = mkPackageOption pkgs "lldap-bootstrap" { };
|
||||
+
|
||||
environment = mkOption {
|
||||
type = with types; attrsOf str;
|
||||
default = { };
|
||||
@@ -146,6 +217,172 @@ in
|
||||
};
|
||||
};
|
||||
};
|
||||
+
|
||||
+ ensureUsers = mkOption {
|
||||
+ description = ''
|
||||
+ Create the users defined here on service startup.
|
||||
+
|
||||
+ If `enforceEnsure` option is `true`, the groups
|
||||
+ users belong to must be present in the `ensureGroups` option.
|
||||
+
|
||||
+ Non-default options must be added to the `ensureGroupFields` option.
|
||||
+ '';
|
||||
+ default = { };
|
||||
+ type = types.attrsOf (
|
||||
+ types.submodule (
|
||||
+ { name, ... }:
|
||||
+ {
|
||||
+ freeformType = ensureFormat.type;
|
||||
+
|
||||
+ options = {
|
||||
+ id = mkOption {
|
||||
+ type = types.str;
|
||||
+ description = "Username.";
|
||||
+ default = name;
|
||||
+ };
|
||||
+
|
||||
+ email = mkOption {
|
||||
+ type = types.str;
|
||||
+ description = "Email.";
|
||||
+ };
|
||||
+
|
||||
+ password_file = mkOption {
|
||||
+ type = types.str;
|
||||
+ description = "File containing the password.";
|
||||
+ };
|
||||
+
|
||||
+ displayName = mkOption {
|
||||
+ type = types.nullOr types.str;
|
||||
+ default = null;
|
||||
+ description = "Display name.";
|
||||
+ };
|
||||
+
|
||||
+ firstName = mkOption {
|
||||
+ type = types.nullOr types.str;
|
||||
+ default = null;
|
||||
+ description = "First name.";
|
||||
+ };
|
||||
+
|
||||
+ lastName = mkOption {
|
||||
+ type = types.nullOr types.str;
|
||||
+ default = null;
|
||||
+ description = "Last name.";
|
||||
+ };
|
||||
+
|
||||
+ avatar_file = mkOption {
|
||||
+ type = types.nullOr types.str;
|
||||
+ default = null;
|
||||
+ description = "Avatar file. Must be a valid path to jpeg file (ignored if avatar_url specified)";
|
||||
+ };
|
||||
+
|
||||
+ avatar_url = mkOption {
|
||||
+ type = types.nullOr types.str;
|
||||
+ default = null;
|
||||
+ description = "Avatar url. must be a valid URL to jpeg file (ignored if gravatar_avatar specified)";
|
||||
+ };
|
||||
+
|
||||
+ gravatar_avatar = mkOption {
|
||||
+ type = types.nullOr types.str;
|
||||
+ default = null;
|
||||
+ description = "Get avatar from Gravatar using the email.";
|
||||
+ };
|
||||
+
|
||||
+ weser_avatar = mkOption {
|
||||
+ type = types.nullOr types.str;
|
||||
+ default = null;
|
||||
+ description = "Convert avatar retrieved by gravatar or the URL.";
|
||||
+ };
|
||||
+
|
||||
+ groups = mkOption {
|
||||
+ type = types.listOf types.str;
|
||||
+ default = [ ];
|
||||
+ description = "Groups the user would be a member of (all the groups must be specified in group config files).";
|
||||
+ };
|
||||
+ };
|
||||
+ }
|
||||
+ )
|
||||
+ );
|
||||
+ };
|
||||
+
|
||||
+ ensureGroups = mkOption {
|
||||
+ description = ''
|
||||
+ Create the groups defined here on service startup.
|
||||
+
|
||||
+ Non-default options must be added to the `ensureGroupFields` option.
|
||||
+ '';
|
||||
+ default = { };
|
||||
+ type = types.attrsOf (
|
||||
+ types.submodule (
|
||||
+ { name, ... }:
|
||||
+ {
|
||||
+ freeformType = ensureFormat.type;
|
||||
+
|
||||
+ options = {
|
||||
+ name = mkOption {
|
||||
+ type = types.str;
|
||||
+ description = "Name of the group.";
|
||||
+ default = name;
|
||||
+ };
|
||||
+ };
|
||||
+ }
|
||||
+ )
|
||||
+ );
|
||||
+ };
|
||||
+
|
||||
+ ensureUserFields = mkOption {
|
||||
+ description = "Extra fields for users";
|
||||
+ default = { };
|
||||
+ type = types.attrsOf (
|
||||
+ types.submodule (
|
||||
+ { name, ... }:
|
||||
+ {
|
||||
+ options = ensureFieldsOptions name;
|
||||
+ }
|
||||
+ )
|
||||
+ );
|
||||
+ };
|
||||
+
|
||||
+ ensureGroupFields = mkOption {
|
||||
+ description = "Extra fields for groups";
|
||||
+ default = { };
|
||||
+ type = types.attrsOf (
|
||||
+ types.submodule (
|
||||
+ { name, ... }:
|
||||
+ {
|
||||
+ options = ensureFieldsOptions name;
|
||||
+ }
|
||||
+ )
|
||||
+ );
|
||||
+ };
|
||||
+
|
||||
+ ensureAdminUsername = mkOption {
|
||||
+ type = types.str;
|
||||
+ default = "admin";
|
||||
+ description = ''
|
||||
+ Username of an admin user with which to connect to the LLDAP service.
|
||||
+
|
||||
+ By default, it is the default admin username `admin`.
|
||||
+ If using another user, it must be managed manually.
|
||||
+ '';
|
||||
+ };
|
||||
+
|
||||
+ ensureAdminPasswordFile = mkOption {
|
||||
+ type = types.nullOr types.str;
|
||||
+ defaultText = "config.services.lldap.adminPasswordFile";
|
||||
+ default = cfg.adminPasswordFile;
|
||||
+ description = ''
|
||||
+ Path to the file containing the password of an admin user with which to connect to the LLDAP service.
|
||||
+
|
||||
+ By default, it is the same as the password for the default admin user 'admin'.
|
||||
+ If using a password from another user, it must be managed manually.
|
||||
+ '';
|
||||
+ };
|
||||
+
|
||||
+ enforceEnsure = mkOption {
|
||||
+ description = "Delete users, groups and fields not in their respective ensure options and remove users from groups they do not belong to.";
|
||||
+ type = types.bool;
|
||||
+ default = false;
|
||||
+ };
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
@@ -158,8 +395,40 @@ in
|
||||
Please set the `resetAdminPassword` option to `true` or `"always"`.
|
||||
'';
|
||||
}
|
||||
+ {
|
||||
+ assertion =
|
||||
+ cfg.ensureUsers != { }
|
||||
+ || cfg.ensureGroups != { }
|
||||
+ || cfg.ensureUserFields != { }
|
||||
+ || cfg.ensureGroupFields != { }
|
||||
+ || cfg.enforceEnsure
|
||||
+ -> cfg.ensureAdminPasswordFile != null;
|
||||
+ message = ''
|
||||
+ Some ensure options are set but no admin user password is set.
|
||||
+ Add a default password to `adminPasswordFile` to manage the admin user declaratively
|
||||
+ or create a user manually and set its password in `ensureAdminPasswordFile`.
|
||||
+ '';
|
||||
+ }
|
||||
+ {
|
||||
+ assertion = cfg.enforceEnsure -> !someUsersBelongToNonEnsuredGroup;
|
||||
+ message = ''
|
||||
+ Some users belong to groups not present in the ensureGroups attr,
|
||||
+ add the following groups or remove them from the groups a user belong to:
|
||||
+ ${lib.concatStringsSep ", " (map (x: "\"${x}\"") userGroupNotInEnsuredGroup)}
|
||||
+ '';
|
||||
+ }
|
||||
];
|
||||
|
||||
+ warnings = (
|
||||
+ lib.optionals (!cfg.enforceEnsure && (lib.debug.traceValSeq someUsersBelongToNonEnsuredGroup)) [
|
||||
+ ''
|
||||
+ Some users belong to groups not managed by the configuration here,
|
||||
+ make sure the following groups exist or the service will not start properly:
|
||||
+ ${lib.concatStringsSep ", " (map (x: "\"${x}\"") userGroupNotInEnsuredGroup)}
|
||||
+ ''
|
||||
+ ]
|
||||
+ );
|
||||
+
|
||||
services.lldap.environment = {
|
||||
LLDAP_JWT_SECRET_FILE = lib.mkIf (cfg.jwtSecretFile != null) cfg.jwtSecretFile;
|
||||
LLDAP_LDAP_USER_PASS_FILE = lib.mkIf (cfg.adminPasswordFile != null) cfg.adminPasswordFile;
|
||||
@@ -193,6 +462,17 @@ in
|
||||
+ ''
|
||||
${lib.getExe cfg.package} run --config-file ${format.generate "lldap_config.toml" cfg.settings}
|
||||
'';
|
||||
+ postStart = ''
|
||||
+ export LLDAP_URL=http://127.0.0.1:${toString cfg.settings.http_port}
|
||||
+ export LLDAP_ADMIN_USERNAME=${cfg.ensureAdminUsername}
|
||||
+ export LLDAP_ADMIN_PASSWORD_FILE=${cfg.ensureAdminPasswordFile}
|
||||
+ export USER_CONFIGS_DIR=${generateEnsureConfigDir "users" cfg.ensureUsers}
|
||||
+ export GROUP_CONFIGS_DIR=${generateEnsureConfigDir "groups" cfg.ensureGroups}
|
||||
+ export USER_SCHEMAS_DIR=${generateEnsureConfigDir "userFields" cfg.ensureUserFields}
|
||||
+ export GROUP_SCHEMAS_DIR=${generateEnsureConfigDir "groupFields" cfg.ensureGroupFields}
|
||||
+ export DO_CLEANUP=${if cfg.enforceEnsure then "true" else "false"}
|
||||
+ ${lib.getExe cfg.bootstrap-package}
|
||||
+ '';
|
||||
serviceConfig = {
|
||||
StateDirectory = "lldap";
|
||||
StateDirectoryMode = "0750";
|
||||
diff --git a/nixos/tests/lldap.nix b/nixos/tests/lldap.nix
|
||||
index e88fa37ab83d..7d2e65699a0f 100644
|
||||
--- a/nixos/tests/lldap.nix
|
||||
+++ b/nixos/tests/lldap.nix
|
||||
@@ -1,6 +1,8 @@
|
||||
{ ... }:
|
||||
let
|
||||
adminPassword = "mySecretPassword";
|
||||
+ alicePassword = "AlicePassword";
|
||||
+ bobPassword = "BobPassword";
|
||||
in
|
||||
{
|
||||
name = "lldap";
|
||||
@@ -19,21 +21,125 @@ in
|
||||
verbose = true;
|
||||
ldap_base_dn = "dc=example,dc=com";
|
||||
};
|
||||
+
|
||||
+ ensureUsers = {
|
||||
+ alice = {
|
||||
+ email = "alice@example.com";
|
||||
+ password_file = toString (pkgs.writeText "alicePasswordFile" alicePassword);
|
||||
+ groups = [ "mygroup" ];
|
||||
+ };
|
||||
+ };
|
||||
+
|
||||
+ ensureGroups = {
|
||||
+ mygroup = { };
|
||||
+ };
|
||||
};
|
||||
environment.systemPackages = [ pkgs.openldap ];
|
||||
+
|
||||
+ specialisation = {
|
||||
+ withAlice.configuration =
|
||||
+ { ... }:
|
||||
+ {
|
||||
+ services.lldap = {
|
||||
+ ensureUsers = {
|
||||
+ alice = {
|
||||
+ email = "alice@example.com";
|
||||
+ password_file = toString (pkgs.writeText "alicePasswordFile" alicePassword);
|
||||
+ groups = [ "mygroup" ];
|
||||
+ };
|
||||
+ };
|
||||
+
|
||||
+ ensureGroups = {
|
||||
+ mygroup = { };
|
||||
+ };
|
||||
+ };
|
||||
+ };
|
||||
+
|
||||
+ withBob.configuration =
|
||||
+ { ... }:
|
||||
+ {
|
||||
+ services.lldap = {
|
||||
+ ensureUsers = {
|
||||
+ bob = {
|
||||
+ email = "bob@example.com";
|
||||
+ password_file = toString (pkgs.writeText "bobPasswordFile" bobPassword);
|
||||
+ groups = [ "othergroup" ];
|
||||
+ displayName = "Bob";
|
||||
+ myattribute = 2;
|
||||
+ };
|
||||
+ };
|
||||
+
|
||||
+ ensureGroups = {
|
||||
+ othergroup = {
|
||||
+ mygroupattribute = "Managed by NixOS";
|
||||
+ };
|
||||
+ };
|
||||
+
|
||||
+ ensureUserFields = {
|
||||
+ myattribute = {
|
||||
+ attributeType = "INTEGER";
|
||||
+ };
|
||||
+ };
|
||||
+
|
||||
+ ensureGroupFields = {
|
||||
+ mygroupattribute = {
|
||||
+ attributeType = "STRING";
|
||||
+ };
|
||||
+ };
|
||||
+ };
|
||||
+ };
|
||||
+ };
|
||||
};
|
||||
|
||||
- testScript = ''
|
||||
+ testScript =
|
||||
+ { nodes, ... }:
|
||||
+ let
|
||||
+ specializations = "${nodes.machine.system.build.toplevel}/specialisation";
|
||||
+ in
|
||||
+ ''
|
||||
machine.wait_for_unit("lldap.service")
|
||||
machine.wait_for_open_port(3890)
|
||||
machine.wait_for_open_port(17170)
|
||||
|
||||
machine.succeed("curl --location --fail http://localhost:17170/")
|
||||
+ adminPassword="${adminPassword}"
|
||||
+ alicePassword="${alicePassword}"
|
||||
+ bobPassword="${bobPassword}"
|
||||
+
|
||||
+ def try_login(user, password, expect_success=True):
|
||||
+ code, response = machine.execute(f'ldapsearch -H ldap://localhost:3890 -D uid={user},ou=people,dc=example,dc=com -b "ou=people,dc=example,dc=com" -w {password}')
|
||||
+ print(response)
|
||||
+ if expect_success:
|
||||
+ if code != 0:
|
||||
+ raise Exception("Expected failure, had success")
|
||||
+ else:
|
||||
+ if code == 0:
|
||||
+ raise Exception(f"Expected success, had failure {code}")
|
||||
+
|
||||
+ with subtest("only default admin user"):
|
||||
+ print(try_login("admin", "password", expect_success=False))
|
||||
+ print(try_login("admin", adminPassword, expect_success=True))
|
||||
+ print(try_login("alice", "password", expect_success=False))
|
||||
+ print(try_login("alice", alicePassword, expect_success=False))
|
||||
+ print(try_login("bob", "password", expect_success=False))
|
||||
+ print(try_login("bob", bobPassword, expect_success=False))
|
||||
|
||||
- response = machine.fail('ldapsearch -H ldap://localhost:3890 -D uid=admin,ou=people,dc=example,dc=com -b "ou=people,dc=example,dc=com" -w password')
|
||||
- print(response)
|
||||
+ with subtest("with alice"):
|
||||
+ machine.succeed('${specializations}/withAlice/bin/switch-to-configuration test')
|
||||
+ print(try_login("admin", "password", expect_success=False))
|
||||
+ print(try_login("admin", adminPassword, expect_success=True))
|
||||
+ print(try_login("alice", "password", expect_success=False))
|
||||
+ print(try_login("alice", alicePassword, expect_success=True))
|
||||
+ print(try_login("bob", "password", expect_success=False))
|
||||
+ print(try_login("bob", bobPassword, expect_success=False))
|
||||
|
||||
- response = machine.succeed('ldapsearch -H ldap://localhost:3890 -D uid=admin,ou=people,dc=example,dc=com -b "ou=people,dc=example,dc=com" -w ${adminPassword}')
|
||||
- print(response)
|
||||
+ with subtest("with attributes"):
|
||||
+ machine.succeed('${specializations}/withBob/bin/switch-to-configuration test')
|
||||
+ print(try_login("admin", "password", expect_success=False))
|
||||
+ print(try_login("admin", adminPassword, expect_success=True))
|
||||
+ print(try_login("alice", "password", expect_success=False))
|
||||
+ print(try_login("alice", alicePassword, expect_success=False))
|
||||
+ print(try_login("bob", "password", expect_success=False))
|
||||
+ print(try_login("bob", bobPassword, expect_success=True))
|
||||
'';
|
||||
}
|
||||
diff --git a/pkgs/by-name/ll/lldap-bootstrap/package.nix b/pkgs/by-name/ll/lldap-bootstrap/package.nix
|
||||
new file mode 100644
|
||||
index 000000000000..1459784bf8e5
|
||||
--- /dev/null
|
||||
+++ b/pkgs/by-name/ll/lldap-bootstrap/package.nix
|
||||
@@ -0,0 +1,54 @@
|
||||
+{
|
||||
+ curl,
|
||||
+ fetchFromGitHub,
|
||||
+ jq,
|
||||
+ jo,
|
||||
+ lib,
|
||||
+ lldap,
|
||||
+ lldap-bootstrap,
|
||||
+ makeWrapper,
|
||||
+ stdenv,
|
||||
+}:
|
||||
+stdenv.mkDerivation {
|
||||
+ pname = "lldap-bootstrap";
|
||||
+ version = "unstable-2025-07-17";
|
||||
+
|
||||
+ src = fetchFromGitHub {
|
||||
+ owner = "ibizaman";
|
||||
+ repo = "lldap";
|
||||
+ rev = "14b083c9cf6c13802b7477af3b85894f2b7d1c81";
|
||||
+ hash = "sha256-olQGLgjLE7la5fYiCgC4tpaxyhFT9ZvRWLtASoaqq9k=";
|
||||
+ };
|
||||
+
|
||||
+ dontBuild = true;
|
||||
+
|
||||
+ nativeBuildInputs = [ makeWrapper ];
|
||||
+
|
||||
+ installPhase = ''
|
||||
+ mkdir -p $out/bin
|
||||
+ cp ./scripts/bootstrap.sh $out/bin/lldap-bootstrap
|
||||
+
|
||||
+ wrapProgram $out/bin/lldap-bootstrap \
|
||||
+ --set LLDAP_SET_PASSWORD_PATH ${lldap}/bin/lldap_set_password \
|
||||
+ --prefix PATH : ${
|
||||
+ lib.makeBinPath [
|
||||
+ curl
|
||||
+ jq
|
||||
+ jo
|
||||
+ ]
|
||||
+ }
|
||||
+ '';
|
||||
+
|
||||
+ meta = {
|
||||
+ description = "Bootstrap script for LLDAP";
|
||||
+ homepage = "https://github.com/lldap/lldap";
|
||||
+ changelog = "https://github.com/lldap/lldap/blob/v${lldap-bootstrap.version}/CHANGELOG.md";
|
||||
+ license = lib.licenses.gpl3Only;
|
||||
+ platforms = lib.platforms.linux;
|
||||
+ maintainers = with lib.maintainers; [
|
||||
+ bendlas
|
||||
+ ibizaman
|
||||
+ ];
|
||||
+ mainProgram = "lldap-bootstrap";
|
||||
+ };
|
||||
+}
|
||||
--
|
||||
2.49.0
|
||||
|
||||
|
|
@ -3,6 +3,7 @@ let
|
|||
pkgs' = pkgs;
|
||||
|
||||
password = "securepassword";
|
||||
charliePassword = "CharliePassword";
|
||||
in
|
||||
{
|
||||
auth = pkgs.testers.runNixOSTest {
|
||||
|
|
@ -29,6 +30,17 @@ in
|
|||
ldapUserPassword.result = config.shb.hardcodedsecret.ldapUserPassword.result;
|
||||
jwtSecret.result = config.shb.hardcodedsecret.jwtSecret.result;
|
||||
debug = true;
|
||||
|
||||
ensureUsers = {
|
||||
"charlie" = {
|
||||
email = "charlie@example.com";
|
||||
password.result = config.shb.hardcodedsecret."charlie".result;
|
||||
};
|
||||
};
|
||||
|
||||
ensureGroups = {
|
||||
"family" = {};
|
||||
};
|
||||
};
|
||||
shb.hardcodedsecret.ldapUserPassword = {
|
||||
request = config.shb.lldap.ldapUserPassword.request;
|
||||
|
|
@ -38,6 +50,10 @@ in
|
|||
request = config.shb.lldap.jwtSecret.request;
|
||||
settings.content = "jwtSecret";
|
||||
};
|
||||
shb.hardcodedsecret."charlie" = {
|
||||
request = config.shb.lldap.ensureUsers."charlie".password.request;
|
||||
settings.content = charliePassword;
|
||||
};
|
||||
|
||||
networking.firewall.allowedTCPPorts = [ 80 ]; # nginx port
|
||||
};
|
||||
|
|
@ -89,6 +105,15 @@ in
|
|||
|
||||
assert data['user']['displayName'] == "Administrator"
|
||||
assert data['user']['groups'][0]['displayName'] == "lldap_admin"
|
||||
|
||||
with subtest("succeed charlie"):
|
||||
client.succeed(
|
||||
"curl -f -s -X POST "
|
||||
+ """ -H "Content-type: application/json" """
|
||||
+ """ -H "Host: ldap.example.com" """
|
||||
+ " http://server/auth/simple/login "
|
||||
+ """ -d '{"username": "charlie", "password": "${charliePassword}"}' """
|
||||
)
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue