diff --git a/flake.nix b/flake.nix index 5f0e527..23cf69c 100644 --- a/flake.nix +++ b/flake.nix @@ -16,9 +16,9 @@ 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 + ./patches/0001-lldap-add-options-to-set-important-secrets.patch + ./patches/0002-lldap-lldap-0.6.1-unstable-2025-07-16.patch + ./patches/0003-lldap-bootstrap-init-unstable-2025-07-17-lldap-add-e.patch # Leaving commented out as an example. # (originPkgs.fetchpatch { diff --git a/modules/blocks/lldap.nix b/modules/blocks/lldap.nix index 57ed5fd..01445af 100644 --- a/modules/blocks/lldap.nix +++ b/modules/blocks/lldap.nix @@ -336,9 +336,6 @@ in services.lldap = { enable = true; - adminPasswordFile = toString cfg.ldapUserPassword.result.path; - jwtSecretFile = toString cfg.jwtSecret.result.path; - resetAdminPassword = "always"; enforceEnsure = true; environment = { @@ -352,9 +349,12 @@ in ldap_host = "127.0.0.1"; ldap_port = cfg.ldapPort; - ldap_base_dn = cfg.dcdomain; + ldap_user_pass_file = toString cfg.ldapUserPassword.result.path; + force_ldap_user_pass_reset = "always"; + jwt_secret_file = toString cfg.jwtSecret.result.path; + verbose = cfg.debug; }; diff --git a/patches/0001-lldap-add-options-to-set-important-secrets.patch b/patches/0001-lldap-add-options-to-set-important-secrets.patch new file mode 100644 index 0000000..a7192ce --- /dev/null +++ b/patches/0001-lldap-add-options-to-set-important-secrets.patch @@ -0,0 +1,197 @@ +From d7b92a627d2c5248cf3934f6b94bc57885a208a4 Mon Sep 17 00:00:00 2001 +From: ibizaman +Date: Wed, 16 Jul 2025 03:04:44 +0200 +Subject: [PATCH 1/3] lldap: add options to set important secrets + +--- + nixos/modules/services/databases/lldap.nix | 71 +++++++++++++++++++++ + nixos/tests/lldap.nix | 72 +++++++++++++++++++--- + 2 files changed, 134 insertions(+), 9 deletions(-) + +diff --git a/nixos/modules/services/databases/lldap.nix b/nixos/modules/services/databases/lldap.nix +index a9fbe8f7e11a..1607754d7d9c 100644 +--- a/nixos/modules/services/databases/lldap.nix ++++ b/nixos/modules/services/databases/lldap.nix +@@ -102,12 +102,83 @@ in + default = "sqlite://./users.db?mode=rwc"; + example = "postgres://postgres-user:password@postgres-server/my-database"; + }; ++ ++ ldap_user_pass_file = mkOption { ++ type = types.nullOr types.str; ++ default = null; ++ description = '' ++ Path to a file containing the default admin password. ++ ++ If you want to update the default admin password through this setting, ++ you must set `force_ldap_user_pass_reset` to `true`. ++ Otherwise changing this setting will have no effect ++ unless this is the very first time LLDAP is started and its database is still empty. ++ ''; ++ }; ++ ++ force_ldap_user_pass_reset = mkOption { ++ type = types.oneOf [ ++ types.bool ++ (types.enum [ "always" ]) ++ ]; ++ default = false; ++ description = '' ++ Force reset of the admin password. ++ ++ Set this setting to `"always"` to update the admin password when `ldap_user_pass_file` changes. ++ Setting to `"always"` also means any password update in the UI will be overwritten next time the service restarts. ++ ++ 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. ++ ''; ++ }; ++ ++ jwt_secret_file = mkOption { ++ type = types.nullOr types.str; ++ default = null; ++ description = '' ++ Path to a file containing the JWT secret. ++ ''; ++ }; + }; + }; ++ ++ # TOML does not allow null values, so we use null to omit those fields ++ apply = lib.filterAttrsRecursive (_: v: v != null); ++ }; ++ ++ silenceForceUserPassResetWarning = mkOption { ++ type = types.bool; ++ default = false; ++ description = '' ++ Disable warning when the admin password is set declaratively with the `ldap_user_pass_file` setting ++ but the `force_ldap_user_pass_reset` is set to `false`. ++ ++ This can lead to the admin password to drift from the one given declaratively. ++ If that is okay for you and you want to silence the warning, set this option to `true`. ++ ''; + }; + }; + + config = lib.mkIf cfg.enable { ++ warnings = ++ lib.optionals ++ ( ++ (cfg.settings.ldap_user_pass_file or null) != null ++ && cfg.settings.force_ldap_user_pass_reset == false ++ && cfg.silenceForceUserPassResetWarning == false ++ ) ++ [ ++ '' ++ lldap: The default admin password is declared with the setting `ldap_user_pass_file`, but `force_ldap_user_pass_reset` 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. ++ It also means changing the setting `ldap_user_pass_file` will have no effect on the admin password. ++ Either set `force_ldap_user_pass_reset` to `"always"` or silence this warning by setting the option `services.lldap.silenceForceUserPassResetWarning` to `true`. ++ '' ++ ]; ++ + 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..aea6b2058727 100644 +--- a/nixos/tests/lldap.nix ++++ b/nixos/tests/lldap.nix +@@ -1,4 +1,7 @@ + { ... }: ++let ++ adminPassword = "mySecretPassword"; ++in + { + name = "lldap"; + +@@ -7,23 +10,74 @@ + { + services.lldap = { + enable = true; ++ + settings = { + verbose = true; + ldap_base_dn = "dc=example,dc=com"; + }; + }; + environment.systemPackages = [ pkgs.openldap ]; ++ ++ specialisation = { ++ differentAdminPassword.configuration = ++ { ... }: ++ { ++ services.lldap.settings = { ++ ldap_user_pass_file = toString (pkgs.writeText "adminPasswordFile" adminPassword); ++ force_ldap_user_pass_reset = "always"; ++ }; ++ }; ++ ++ changeAdminPassword.configuration = ++ { ... }: ++ { ++ services.lldap.settings = { ++ ldap_user_pass_file = toString (pkgs.writeText "adminPasswordFile" "password"); ++ force_ldap_user_pass_reset = false; ++ }; ++ }; ++ }; + }; + +- testScript = '' +- machine.wait_for_unit("lldap.service") +- machine.wait_for_open_port(3890) +- machine.wait_for_open_port(17170) ++ 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}" ++ ++ def try_login(user, password, expect_success=True): ++ cmd = f'ldapsearch -H ldap://localhost:3890 -D uid={user},ou=people,dc=example,dc=com -b "ou=people,dc=example,dc=com" -w {password}' ++ code, response = machine.execute(cmd) ++ print(cmd) ++ print(response) ++ if expect_success: ++ if code != 0: ++ raise Exception(f"Expected success, had failure {code}") ++ else: ++ if code == 0: ++ raise Exception("Expected failure, had success") ++ return response ++ ++ with subtest("default admin password"): ++ try_login("admin", "password", expect_success=True) ++ try_login("admin", adminPassword, expect_success=False) + +- machine.succeed("curl --location --fail http://localhost:17170/") ++ with subtest("different admin password"): ++ machine.succeed('${specializations}/differentAdminPassword/bin/switch-to-configuration test') ++ try_login("admin", "password", expect_success=False) ++ try_login("admin", adminPassword, expect_success=True) + +- 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') +- ) +- ''; ++ with subtest("change admin password has no effect"): ++ machine.succeed('${specializations}/differentAdminPassword/bin/switch-to-configuration test') ++ try_login("admin", "password", expect_success=False) ++ try_login("admin", adminPassword, expect_success=True) ++ ''; + } +-- +2.49.0 + diff --git a/patches/0002-lldap-add-options-to-set-important-secrets.patch b/patches/0002-lldap-add-options-to-set-important-secrets.patch deleted file mode 100644 index 7c1d48d..0000000 --- a/patches/0002-lldap-add-options-to-set-important-secrets.patch +++ /dev/null @@ -1,133 +0,0 @@ -From f898e786bfb07399abf4e8173ee525c57eb41984 Mon Sep 17 00:00:00 2001 -From: ibizaman -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 - diff --git a/patches/0001-lldap-lldap-0.6.1-unstable-2025-07-16.patch b/patches/0002-lldap-lldap-0.6.1-unstable-2025-07-16.patch similarity index 54% rename from patches/0001-lldap-lldap-0.6.1-unstable-2025-07-16.patch rename to patches/0002-lldap-lldap-0.6.1-unstable-2025-07-16.patch index f6fcbca..1ab1b4a 100644 --- a/patches/0001-lldap-lldap-0.6.1-unstable-2025-07-16.patch +++ b/patches/0002-lldap-lldap-0.6.1-unstable-2025-07-16.patch @@ -1,14 +1,127 @@ -From 5bc3588204e71a77d7e0bfccc4cb70e6ccfbb54b Mon Sep 17 00:00:00 2001 +From ac7fcd01a20cfd93e2ac2b558f38bfa500e5f28a Mon Sep 17 00:00:00 2001 From: ibizaman Date: Tue, 15 Jul 2025 18:42:42 +0200 -Subject: [PATCH 1/3] lldap: lldap 0.6.1 -> unstable-2025-07-16 +Subject: [PATCH 2/3] lldap: lldap 0.6.1 -> unstable-2025-07-16 --- + nixos/modules/services/databases/lldap.nix | 43 ++++++++++--- + nixos/tests/lldap.nix | 8 ++- .../0001-parameterize-frontend-location.patch | 64 ------------------- pkgs/by-name/ll/lldap/package.nix | 30 +++++---- - 2 files changed, 16 insertions(+), 78 deletions(-) + 4 files changed, 55 insertions(+), 90 deletions(-) delete mode 100644 pkgs/by-name/ll/lldap/0001-parameterize-frontend-location.patch +diff --git a/nixos/modules/services/databases/lldap.nix b/nixos/modules/services/databases/lldap.nix +index 1607754d7d9c..1095021b3f35 100644 +--- a/nixos/modules/services/databases/lldap.nix ++++ b/nixos/modules/services/databases/lldap.nix +@@ -2,7 +2,6 @@ + config, + lib, + pkgs, +- utils, + ... + }: + +@@ -103,6 +102,16 @@ in + example = "postgres://postgres-user:password@postgres-server/my-database"; + }; + ++ ldap_user_pass = mkOption { ++ type = types.nullOr types.str; ++ default = null; ++ description = '' ++ Password for default admin password. ++ ++ Unsecure: Use `ldap_user_pass_file` settings instead. ++ ''; ++ }; ++ + ldap_user_pass_file = mkOption { + type = types.nullOr types.str; + default = null; +@@ -163,18 +172,32 @@ in + }; + + config = lib.mkIf cfg.enable { ++ assertions = [ ++ { ++ assertion = ++ (cfg.settings.ldap_user_pass_file or null) != null || (cfg.settings.ldap_user_pass or null) != null; ++ message = "lldap: Default admin user password must be set. Please set the `ldap_user_pass` or better the `ldap_user_pass_file` setting."; ++ } ++ { ++ assertion = ++ (cfg.settings.ldap_user_pass_file or null) == null || (cfg.settings.ldap_user_pass or null) == null; ++ message = "lldap: Both `ldap_user_pass` and `ldap_user_pass_file` settings should not be set at the same time. Set one to `null`."; ++ } ++ ]; ++ + warnings = +- lib.optionals +- ( +- (cfg.settings.ldap_user_pass_file or null) != null +- && cfg.settings.force_ldap_user_pass_reset == false +- && cfg.silenceForceUserPassResetWarning == false +- ) ++ lib.optionals (cfg.settings.ldap_user_pass or null != null) [ ++ '' ++ lldap: Unsecure `ldap_user_pass` setting is used. Prefer `ldap_user_pass_file` instead. ++ '' ++ ] ++ ++ lib.optionals ++ (cfg.settings.force_ldap_user_pass_reset == false && cfg.silenceForceUserPassResetWarning == false) + [ + '' +- lldap: The default admin password is declared with the setting `ldap_user_pass_file`, but `force_ldap_user_pass_reset` 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. +- It also means changing the setting `ldap_user_pass_file` will have no effect on the admin password. ++ lldap: The `force_ldap_user_pass_reset` setting is set to `false` which means ++ the admin password can be changed through the UI and will drift from the one defined in your nix config. ++ It also means changing the setting `ldap_user_pass` or `ldap_user_pass_file` will have no effect on the admin password. + Either set `force_ldap_user_pass_reset` to `"always"` or silence this warning by setting the option `services.lldap.silenceForceUserPassResetWarning` to `true`. + '' + ]; +diff --git a/nixos/tests/lldap.nix b/nixos/tests/lldap.nix +index aea6b2058727..8e38d4bdefa3 100644 +--- a/nixos/tests/lldap.nix ++++ b/nixos/tests/lldap.nix +@@ -6,7 +6,7 @@ in + name = "lldap"; + + nodes.machine = +- { pkgs, ... }: ++ { pkgs, lib, ... }: + { + services.lldap = { + enable = true; +@@ -14,6 +14,8 @@ in + settings = { + verbose = true; + ldap_base_dn = "dc=example,dc=com"; ++ ++ ldap_user_pass = "password"; + }; + }; + environment.systemPackages = [ pkgs.openldap ]; +@@ -23,7 +25,8 @@ in + { ... }: + { + services.lldap.settings = { +- ldap_user_pass_file = toString (pkgs.writeText "adminPasswordFile" adminPassword); ++ ldap_user_pass = lib.mkForce null; ++ ldap_user_pass_file = lib.mkForce (toString (pkgs.writeText "adminPasswordFile" adminPassword)); + force_ldap_user_pass_reset = "always"; + }; + }; +@@ -32,6 +35,7 @@ in + { ... }: + { + services.lldap.settings = { ++ ldap_user_pass = lib.mkForce null; + ldap_user_pass_file = toString (pkgs.writeText "adminPasswordFile" "password"); + force_ldap_user_pass_reset = false; + }; 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 diff --git a/patches/0003-lldap-bootstrap-init-unstable-2025-07-16-lldap-add-e.patch b/patches/0003-lldap-bootstrap-init-unstable-2025-07-17-lldap-add-e.patch similarity index 50% rename from patches/0003-lldap-bootstrap-init-unstable-2025-07-16-lldap-add-e.patch rename to patches/0003-lldap-bootstrap-init-unstable-2025-07-17-lldap-add-e.patch index ede2b49..a265314 100644 --- a/patches/0003-lldap-bootstrap-init-unstable-2025-07-16-lldap-add-e.patch +++ b/patches/0003-lldap-bootstrap-init-unstable-2025-07-17-lldap-add-e.patch @@ -1,28 +1,22 @@ -From 75b9437558a22eabff74339569f98b567f0a0d04 Mon Sep 17 00:00:00 2001 +From 7f30b1ad931cf9af3a1fa3caded057f9b71672bc Mon Sep 17 00:00:00 2001 From: ibizaman 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 +++++++- + nixos/modules/services/databases/lldap.nix | 339 +++++++++++++++++++- + nixos/tests/lldap.nix | 137 +++++++- pkgs/by-name/ll/lldap-bootstrap/package.nix | 54 ++++ - 3 files changed, 446 insertions(+), 6 deletions(-) + pkgs/by-name/ll/lldap/package.nix | 6 +- + 4 files changed, 525 insertions(+), 11 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 +index 1095021b3f35..b23e89ba6794 100644 --- a/nixos/modules/services/databases/lldap.nix +++ b/nixos/modules/services/databases/lldap.nix -@@ -2,13 +2,82 @@ - config, - lib, - pkgs, -- utils, - ... - }: - +@@ -8,6 +8,84 @@ let cfg = config.services.lldap; format = pkgs.formats.toml { }; @@ -31,7 +25,13 @@ index 518b39ba7a86..b7c4c85f751f 100644 + + ensureFormat = pkgs.formats.json { }; + ensureGenerate = -+ name: source: ensureFormat.generate name (lib.filterAttrsRecursive (n: v: v != null) source); ++ let ++ filterNulls = lib.filterAttrsRecursive (n: v: v != null); ++ ++ filteredSource = ++ source: if builtins.isList source then map filterNulls source else filterNulls source; ++ in ++ name: source: ensureFormat.generate name (filteredSource source); + + ensureFieldsOptions = name: { + name = mkOption { @@ -96,10 +96,12 @@ index 518b39ba7a86..b7c4c85f751f 100644 + paths = lib.mapAttrsToList genOne source; + } + }/configs"; ++ ++ quoteVariable = x: "\"${x}\""; in { options.services.lldap = with lib; { -@@ -16,6 +85,8 @@ in +@@ -15,6 +93,8 @@ in package = mkPackageOption pkgs "lldap" { }; @@ -108,9 +110,9 @@ index 518b39ba7a86..b7c4c85f751f 100644 environment = mkOption { type = with types; attrsOf str; default = { }; -@@ -146,6 +217,172 @@ in - }; - }; +@@ -169,6 +249,186 @@ in + If that is okay for you and you want to silence the warning, set this option to `true`. + ''; }; + + ensureUsers = mkOption { @@ -254,17 +256,31 @@ index 518b39ba7a86..b7c4c85f751f 100644 + type = types.str; + default = "admin"; + description = '' -+ Username of an admin user with which to connect to the LLDAP service. ++ Username of the default 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. ++ By default, it is `"admin"`. ++ Extra admin users can be added using the `services.lldap.ensureUsers` option and adding them to the correct groups. ++ ''; ++ }; ++ ++ ensureAdminPassword = mkOption { ++ type = types.nullOr types.str; ++ defaultText = "config.services.lldap.settings.ldap_user_pass"; ++ default = cfg.settings.ldap_user_pass or null; ++ description = '' ++ 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. ++ ++ Unsecure. Use `services.lldap.ensureAdminPasswordFile` option instead. + ''; + }; + + ensureAdminPasswordFile = mkOption { + type = types.nullOr types.str; -+ defaultText = "config.services.lldap.adminPasswordFile"; -+ default = cfg.adminPasswordFile; ++ defaultText = "config.services.lldap.settings.ldap_user_pass_file"; ++ default = cfg.settings.ldap_user_pass_file or null; + description = '' + Path to the file containing the password of an admin user with which to connect to the LLDAP service. + @@ -281,9 +297,9 @@ index 518b39ba7a86..b7c4c85f751f 100644 }; config = lib.mkIf cfg.enable { -@@ -158,8 +395,40 @@ in - Please set the `resetAdminPassword` option to `true` or `"always"`. - ''; +@@ -183,15 +443,58 @@ in + (cfg.settings.ldap_user_pass_file or null) == null || (cfg.settings.ldap_user_pass or null) == null; + message = "lldap: Both `ldap_user_pass` and `ldap_user_pass_file` settings should not be set at the same time. Set one to `null`."; } + { + assertion = @@ -292,48 +308,95 @@ index 518b39ba7a86..b7c4c85f751f 100644 + || cfg.ensureUserFields != { } + || cfg.ensureGroupFields != { } + || cfg.enforceEnsure -+ -> cfg.ensureAdminPasswordFile != null; ++ -> cfg.ensureAdminPassword != null || 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`. ++ lldap: Some ensure options are set but no admin user password is set. ++ Add a default password to the `ldap_user_pass` or `ldap_user_pass_file` setting and set `force_ldap_user_pass_reset` to `true` to manage the admin user declaratively ++ or create an admin user manually and set its password in `ensureAdminPasswordFile` option. + ''; + } + { + assertion = cfg.enforceEnsure -> !someUsersBelongToNonEnsuredGroup; + message = '' -+ Some users belong to groups not present in the ensureGroups attr, ++ lldap: 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)} ++ ${lib.concatMapStringsSep quoteVariable ", " userGroupNotInEnsuredGroup} + ''; + } ++ ( ++ let ++ getNames = source: lib.flatten (lib.mapAttrsToList (x: v: v.name) source); ++ allNames = getNames cfg.ensureUserFields ++ getNames cfg.ensureGroupFields; ++ validFieldName = name: lib.match "[a-zA-Z0-9-]+" name != null; ++ in ++ { ++ assertion = lib.all validFieldName allNames; ++ message = '' ++ lldap: The following custom user or group fields have invalid names. Valid characters are: a-z, A-Z, 0-9, and dash (-). ++ The offending fields are: ${ ++ lib.concatMapStringsSep quoteVariable ", " (lib.filter (x: !(validFieldName x)) allNames) ++ } ++ ''; ++ } ++ ) ]; -+ warnings = ( -+ lib.optionals (!cfg.enforceEnsure && (lib.debug.traceValSeq someUsersBelongToNonEnsuredGroup)) [ + warnings = +- lib.optionals (cfg.settings.ldap_user_pass or null != null) [ ++ (lib.optionals (cfg.ensureAdminPassword != null) [ ++ '' ++ lldap: Unsecure option `ensureAdminPassword` is used. Prefer `ensureAdminPasswordFile` instead. ++ '' ++ ]) ++ ++ (lib.optionals (cfg.settings.ldap_user_pass or null != null) [ + '' + lldap: Unsecure `ldap_user_pass` setting is used. Prefer `ldap_user_pass_file` instead. + '' +- ] +- ++ lib.optionals ++ ]) ++ ++ (lib.optionals + (cfg.settings.force_ldap_user_pass_reset == false && cfg.silenceForceUserPassResetWarning == false) + [ + '' +@@ -200,7 +503,15 @@ in + It also means changing the setting `ldap_user_pass` or `ldap_user_pass_file` will have no effect on the admin password. + Either set `force_ldap_user_pass_reset` to `"always"` or silence this warning by setting the option `services.lldap.silenceForceUserPassResetWarning` to `true`. + '' +- ]; ++ ] ++ ) ++ ++ (lib.optionals (!cfg.enforceEnsure && 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 ++ ]); + + systemd.services.lldap = { + description = "Lightweight LDAP server (lldap)"; +@@ -223,6 +534,26 @@ 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 LLDAP_ADMIN_PASSWORD=${ ++ if cfg.ensureAdminPassword != null then cfg.ensureAdminPassword else "" ++ } ++ export LLDAP_ADMIN_PASSWORD_FILE=${ ++ if cfg.ensureAdminPasswordFile != null then cfg.ensureAdminPasswordFile else "" ++ } ++ export USER_CONFIGS_DIR=${lib.traceVal (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 USER_SCHEMAS_DIR=${ ++ generateEnsureConfigDir "userFields" (lib.mapAttrs (n: v: [ v ]) cfg.ensureUserFields) ++ } ++ export GROUP_SCHEMAS_DIR=${ ++ generateEnsureConfigDir "groupFields" (lib.mapAttrs (n: v: [ v ]) cfg.ensureGroupFields) ++ } + export DO_CLEANUP=${if cfg.enforceEnsure then "true" else "false"} + ${lib.getExe cfg.bootstrap-package} + ''; @@ -341,42 +404,42 @@ index 518b39ba7a86..b7c4c85f751f 100644 StateDirectory = "lldap"; StateDirectoryMode = "0750"; diff --git a/nixos/tests/lldap.nix b/nixos/tests/lldap.nix -index e88fa37ab83d..7d2e65699a0f 100644 +index 8e38d4bdefa3..e0bc2fdf07fd 100644 --- a/nixos/tests/lldap.nix +++ b/nixos/tests/lldap.nix -@@ -1,6 +1,8 @@ +@@ -1,6 +1,9 @@ { ... }: let adminPassword = "mySecretPassword"; + alicePassword = "AlicePassword"; + bobPassword = "BobPassword"; ++ charliePassword = "CharliePassword"; in { name = "lldap"; -@@ -19,21 +21,125 @@ in - verbose = true; - ldap_base_dn = "dc=example,dc=com"; - }; +@@ -26,7 +29,7 @@ in + { + services.lldap.settings = { + ldap_user_pass = lib.mkForce null; +- ldap_user_pass_file = lib.mkForce (toString (pkgs.writeText "adminPasswordFile" adminPassword)); ++ ldap_user_pass_file = toString (pkgs.writeText "adminPasswordFile" adminPassword); + force_ldap_user_pass_reset = "always"; + }; + }; +@@ -40,13 +43,104 @@ in + force_ldap_user_pass_reset = false; + }; + }; + -+ 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 = { ++ enforceEnsure = true; ++ ++ # This password was set in the "differentAdminPassword" specialisation. ++ ensureAdminPasswordFile = toString (pkgs.writeText "adminPasswordFile" adminPassword); ++ + ensureUsers = { + alice = { + email = "alice@example.com"; @@ -395,12 +458,46 @@ index e88fa37ab83d..7d2e65699a0f 100644 + { ... }: + { + services.lldap = { ++ enforceEnsure = true; ++ ++ # This time we check that ensureAdminPasswordFile correctly defaults to `settings.ldap_user_pass_file` ++ settings = { ++ ldap_user_pass = lib.mkForce "password"; ++ force_ldap_user_pass_reset = "always"; ++ }; ++ + ensureUsers = { + bob = { + email = "bob@example.com"; + password_file = toString (pkgs.writeText "bobPasswordFile" bobPassword); -+ groups = [ "othergroup" ]; ++ groups = [ "bobgroup" ]; + displayName = "Bob"; ++ }; ++ }; ++ ++ ensureGroups = { ++ bobgroup = { }; ++ }; ++ }; ++ }; ++ ++ withAttributes.configuration = ++ { ... }: ++ { ++ services.lldap = { ++ enforceEnsure = true; ++ ++ settings = { ++ ldap_user_pass = lib.mkForce adminPassword; ++ force_ldap_user_pass_reset = "always"; ++ }; ++ ++ ensureUsers = { ++ charlie = { ++ email = "charlie@example.com"; ++ password_file = toString (pkgs.writeText "charliePasswordFile" charliePassword); ++ groups = [ "othergroup" ]; ++ displayName = "Charlie"; + myattribute = 2; + }; + }; @@ -424,69 +521,83 @@ index e88fa37ab83d..7d2e65699a0f 100644 + }; + }; + }; -+ }; + }; }; -- 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) + testScript = + { nodes, ... }: + let +- specializations = "${nodes.machine.system.build.toplevel}/specialisation"; ++ specialisations = "${nodes.machine.system.build.toplevel}/specialisation"; + in + '' + machine.wait_for_unit("lldap.service") +@@ -56,6 +150,9 @@ in + machine.succeed("curl --location --fail http://localhost:17170/") - machine.succeed("curl --location --fail http://localhost:17170/") -+ adminPassword="${adminPassword}" -+ alicePassword="${alicePassword}" -+ bobPassword="${bobPassword}" + adminPassword="${adminPassword}" ++ alicePassword="${alicePassword}" ++ bobPassword="${bobPassword}" ++ charliePassword="${charliePassword}" + + def try_login(user, password, expect_success=True): + cmd = f'ldapsearch -H ldap://localhost:3890 -D uid={user},ou=people,dc=example,dc=com -b "ou=people,dc=example,dc=com" -w {password}' +@@ -70,18 +167,50 @@ in + raise Exception("Expected failure, had success") + return response + ++ def parse_ldapsearch_output(output): ++ return {n:v for (n, v) in (x.split(': ', 2) for x in output.splitlines() if x != "")} + -+ 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("default admin password"): + try_login("admin", "password", expect_success=True) + try_login("admin", adminPassword, expect_success=False) + + with subtest("different admin password"): +- machine.succeed('${specializations}/differentAdminPassword/bin/switch-to-configuration test') ++ machine.succeed('${specialisations}/differentAdminPassword/bin/switch-to-configuration test') + try_login("admin", "password", expect_success=False) + try_login("admin", adminPassword, expect_success=True) + + with subtest("change admin password has no effect"): +- machine.succeed('${specializations}/differentAdminPassword/bin/switch-to-configuration test') ++ machine.succeed('${specialisations}/differentAdminPassword/bin/switch-to-configuration test') + try_login("admin", "password", expect_success=False) + try_login("admin", adminPassword, expect_success=True) + -+ 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)) - ''; ++ with subtest("with alice"): ++ machine.succeed('${specialisations}/withAlice/bin/switch-to-configuration test') ++ try_login("alice", "password", expect_success=False) ++ try_login("alice", alicePassword, expect_success=True) ++ try_login("bob", "password", expect_success=False) ++ try_login("bob", bobPassword, expect_success=False) ++ ++ with subtest("with bob"): ++ machine.succeed('${specialisations}/withBob/bin/switch-to-configuration test') ++ try_login("alice", "password", expect_success=False) ++ try_login("alice", alicePassword, expect_success=False) ++ try_login("bob", "password", expect_success=False) ++ try_login("bob", bobPassword, expect_success=True) ++ ++ with subtest("with attributes"): ++ machine.succeed('${specialisations}/withAttributes/bin/switch-to-configuration test') ++ ++ response = machine.succeed(f'ldapsearch -LLL -H ldap://localhost:3890 -D uid=admin,ou=people,dc=example,dc=com -b "dc=example,dc=com" -w {adminPassword} "(uid=charlie)"') ++ print(response) ++ charlie = parse_ldapsearch_output(response) ++ if charlie.get('myattribute') != "2": ++ raise Exception(f'Unexpected value for attribute "myattribute": {charlie.get('myattribute')}') ++ ++ response = machine.succeed(f'ldapsearch -LLL -H ldap://localhost:3890 -D uid=admin,ou=people,dc=example,dc=com -b "dc=example,dc=com" -w {adminPassword} "(cn=othergroup)"') ++ print(response) ++ othergroup = parse_ldapsearch_output(response) ++ if othergroup.get('mygroupattribute') != "Managed by NixOS": ++ raise Exception(f'Unexpected value for attribute "mygroupattribute": {othergroup.get('mygroupattribute')}') + ''; } 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 +index 000000000000..d31bad79ce67 --- /dev/null +++ b/pkgs/by-name/ll/lldap-bootstrap/package.nix @@ -0,0 +1,54 @@ @@ -508,8 +619,8 @@ index 000000000000..1459784bf8e5 + src = fetchFromGitHub { + owner = "ibizaman"; + repo = "lldap"; -+ rev = "14b083c9cf6c13802b7477af3b85894f2b7d1c81"; -+ hash = "sha256-olQGLgjLE7la5fYiCgC4tpaxyhFT9ZvRWLtASoaqq9k="; ++ rev = "649a7023da17df70df88152844d2aa1ba0b64440"; ++ hash = "sha256-89SoFqSm3dwVcPQM3lulY26o411NbdS6gTuolw2+e+U="; + }; + + dontBuild = true; @@ -544,6 +655,24 @@ index 000000000000..1459784bf8e5 + mainProgram = "lldap-bootstrap"; + }; +} +diff --git a/pkgs/by-name/ll/lldap/package.nix b/pkgs/by-name/ll/lldap/package.nix +index 41e4a332018e..dc145bbb3fa5 100644 +--- a/pkgs/by-name/ll/lldap/package.nix ++++ b/pkgs/by-name/ll/lldap/package.nix +@@ -19,10 +19,10 @@ let + version = "unstable-2025-07-16"; + + src = fetchFromGitHub { +- owner = "lldap"; ++ owner = "ibizaman"; + repo = "lldap"; +- rev = "78337bce722c3573d9fc6eafe345a3dbce4b9119"; +- hash = "sha256-/djLboAQwK/KQ0u9vzoOdDHwh/BQSvMa8lQkABn10Cw="; ++ rev = "93922b7b0f7f8ac294151ec61b9b21e50e504ab5"; ++ hash = "sha256-p2PUaaD6OrQ+eCkcDZd6x61gERQFQSDEkxwl2s6y8rY="; + }; + + useFetchCargoVendor = true; -- 2.49.0