diff --git a/nixos/modules/misc/ids.nix b/nixos/modules/misc/ids.nix
index bafa2225040..cf0198d7b93 100644
--- a/nixos/modules/misc/ids.nix
+++ b/nixos/modules/misc/ids.nix
@@ -135,7 +135,7 @@ in
#keys = 96; # unused
#haproxy = 97; # dynamically allocated as of 2020-03-11
mongodb = 98;
- openldap = 99;
+ #openldap = 99; # dynamically allocated as of PR#94610
#users = 100; # unused
cgminer = 101;
munin = 102;
@@ -451,7 +451,7 @@ in
keys = 96;
#haproxy = 97; # dynamically allocated as of 2020-03-11
#mongodb = 98; # unused
- openldap = 99;
+ #openldap = 99; # dynamically allocated as of PR#94610
munin = 102;
#logcheck = 103; # unused
#nix-ssh = 104; # unused
diff --git a/nixos/modules/services/databases/openldap.nix b/nixos/modules/services/databases/openldap.nix
index 7472538b887..afe24597e03 100644
--- a/nixos/modules/services/databases/openldap.nix
+++ b/nixos/modules/services/databases/openldap.nix
@@ -1,20 +1,19 @@
{ config, lib, pkgs, ... }:
with lib;
-
let
-
cfg = config.services.openldap;
openldap = cfg.package;
dataFile = pkgs.writeText "ldap-contents.ldif" cfg.declarativeContents;
- configFile = pkgs.writeText "slapd.conf" ((optionalString cfg.defaultSchemas ''
- include ${openldap.out}/etc/schema/core.schema
- include ${openldap.out}/etc/schema/cosine.schema
- include ${openldap.out}/etc/schema/inetorgperson.schema
- include ${openldap.out}/etc/schema/nis.schema
+ configFile = pkgs.writeText "slapd.conf" ((optionalString (cfg.defaultSchemas != null && cfg.defaultSchemas) ''
+ include ${openldap}/etc/schema/core.schema
+ include ${openldap}/etc/schema/cosine.schema
+ include ${openldap}/etc/schema/inetorgperson.schema
+ include ${openldap}/etc/schema/nis.schema
'') + ''
- ${cfg.extraConfig}
+ pidfile /run/slapd/slapd.pid
+ ${if cfg.extraConfig != null then cfg.extraConfig else ""}
database ${cfg.database}
suffix ${cfg.suffix}
rootdn ${cfg.rootdn}
@@ -24,20 +23,79 @@ let
include ${cfg.rootpwFile}
''}
directory ${cfg.dataDir}
- ${cfg.extraDatabaseConfig}
+ ${if cfg.extraDatabaseConfig != null then cfg.extraDatabaseConfig else ""}
'');
- configOpts = if cfg.configDir == null then "-f ${configFile}"
- else "-F ${cfg.configDir}";
-in
-{
+ configDir = if cfg.configDir != null then cfg.configDir else "/etc/openldap/slapd.d";
- ###### interface
+ ldapValueType = let
+ singleLdapValueType = types.either types.str (types.submodule {
+ options = {
+ path = mkOption {
+ type = types.path;
+ description = ''
+ A path containing the LDAP attribute. This is included at run-time, so
+ is recommended for storing secrets.
+ '';
+ };
+ };
+ });
+ in types.either singleLdapValueType (types.listOf singleLdapValueType);
+ ldapAttrsType =
+ let
+ options = {
+ attrs = mkOption {
+ type = types.attrsOf ldapValueType;
+ default = {};
+ description = "Attributes of the parent entry.";
+ };
+ children = mkOption {
+ # Hide the child attributes, to avoid infinite recursion in e.g. documentation
+ # Actual Nix evaluation is lazy, so this is not an issue there
+ type = let
+ hiddenOptions = lib.mapAttrs (name: attr: attr // { visible = false; }) options;
+ in types.attrsOf (types.submodule { options = hiddenOptions; });
+ default = {};
+ description = "Child entries of the current entry, with recursively the same structure.";
+ example = lib.literalExample ''
+ {
+ "cn=schema" = {
+ # The attribute used in the DN must be defined
+ attrs = { cn = "schema"; };
+ children = {
+ # This entry's DN is expanded to "cn=foo,cn=schema"
+ "cn=foo" = { ... };
+ };
+ # These includes are inserted after "cn=schema", but before "cn=foo,cn=schema"
+ includes = [ ... ];
+ };
+ }
+ '';
+ };
+ includes = mkOption {
+ type = types.listOf types.path;
+ default = [];
+ description = ''
+ LDIF files to include after the parent's attributes but before its children.
+ '';
+ };
+ };
+ in types.submodule { inherit options; };
+
+ valueToLdif = attr: values: let
+ singleValueToLdif = value: if lib.isAttrs value then "${attr}:< file://${value.path}" else "${attr}: ${value}";
+ in if lib.isList values then map singleValueToLdif values else [ (singleValueToLdif values) ];
+
+ attrsToLdif = dn: { attrs, children, includes, ... }: [''
+ dn: ${dn}
+ ${lib.concatStringsSep "\n" (lib.flatten (lib.mapAttrsToList valueToLdif attrs))}
+ ''] ++ (map (path: "include: file://${path}\n") includes) ++ (
+ lib.flatten (lib.mapAttrsToList (name: value: attrsToLdif "${name},${dn}" value) children)
+ );
+in {
options = {
-
services.openldap = {
-
enable = mkOption {
type = types.bool;
default = false;
@@ -77,47 +135,91 @@ in
example = [ "ldaps:///" ];
};
+ settings = mkOption {
+ type = ldapAttrsType;
+ description = "Configuration for OpenLDAP, in OLC format";
+ example = lib.literalExample ''
+ {
+ attrs.olcLogLevel = [ "stats" ];
+ children = {
+ "cn=schema".includes = [
+ "\${pkgs.openldap}/etc/schema/core.ldif"
+ "\${pkgs.openldap}/etc/schema/cosine.ldif"
+ "\${pkgs.openldap}/etc/schema/inetorgperson.ldif"
+ ];
+ "olcDatabase={-1}frontend" = {
+ attrs = {
+ objectClass = "olcDatabaseConfig";
+ olcDatabase = "{-1}frontend";
+ olcAccess = [ "{0}to * by dn.exact=uidNumber=0+gidNumber=0,cn=peercred,cn=external,cn=auth manage stop by * none stop" ];
+ };
+ };
+ "olcDatabase={0}config" = {
+ attrs = {
+ objectClass = "olcDatabaseConfig";
+ olcDatabase = "{0}config";
+ olcAccess = [ "{0}to * by * none break" ];
+ };
+ };
+ "olcDatabase={1}mdb" = {
+ attrs = {
+ objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ];
+ olcDatabase = "{1}mdb";
+ olcDbDirectory = "/var/db/ldap";
+ olcDbIndex = [
+ "objectClass eq"
+ "cn pres,eq"
+ "uid pres,eq"
+ "sn pres,eq,subany"
+ ];
+ olcSuffix = "dc=example,dc=com";
+ olcAccess = [ "{0}to * by * read break" ];
+ };
+ };
+ };
+ };
+ '';
+ };
+
+ # These options are translated into settings
dataDir = mkOption {
- type = types.path;
+ type = types.nullOr types.path;
default = "/var/db/openldap";
description = "The database directory.";
};
defaultSchemas = mkOption {
- type = types.bool;
+ type = types.nullOr types.bool;
default = true;
+
description = ''
Include the default schemas core, cosine, inetorgperson and nis.
- This setting will be ignored if configDir is set.
'';
};
database = mkOption {
- type = types.str;
+ type = types.nullOr types.str;
default = "mdb";
- description = ''
- Database type to use for the LDAP.
- This setting will be ignored if configDir is set.
- '';
+ description = "Backend to use for the first database.";
};
suffix = mkOption {
- type = types.str;
+ type = types.nullOr types.str;
+ default = null;
example = "dc=example,dc=org";
description = ''
- Specify the DN suffix of queries that will be passed to this backend
- database.
- This setting will be ignored if configDir is set.
+ Specify the DN suffix of queries that will be passed to the first
+ database database.
'';
};
rootdn = mkOption {
- type = types.str;
+ type = types.nullOr types.str;
+ default = null;
example = "cn=admin,dc=example,dc=org";
description = ''
Specify the distinguished name that is not subject to access control
or administrative limit restrictions for operations on this database.
- This setting will be ignored if configDir is set.
'';
};
@@ -125,10 +227,9 @@ in
type = types.nullOr types.str;
default = null;
description = ''
- Password for the root user.
- This setting will be ignored if configDir is set.
- Using this option will store the root password in plain text in the
- world-readable nix store. To avoid this the rootpwFile can be used.
+ Password for the root user.Using this option will store the root
+ password in plain text in the world-readable nix store. To avoid this
+ the rootpwFile can be used.
'';
};
@@ -137,25 +238,36 @@ in
default = null;
description = ''
Password file for the root user.
- The file should contain the string rootpw followed by the password.
- e.g.: rootpw mysecurepassword
+
+ If the deprecated extraConfig or
+ extraDatabaseConfig options are set, this should
+ contain rootpw followed by the password
+ (e.g. rootpw thePasswordHere).
+
+ Otherwise the file should contain only the password (no trailing
+ newline or leading rootpw).
'';
};
logLevel = mkOption {
- type = types.str;
- default = "0";
- example = "acl trace";
- description = "The log level selector of slapd.";
+ type = types.nullOr (types.listOf types.str);
+ default = null;
+ example = literalExample "[ \"acl\" \"trace\" ]";
+ description = "The log level.";
};
+ # This option overrides settings
configDir = mkOption {
type = types.nullOr types.path;
default = null;
- description = "Use this optional config directory instead of using slapd.conf";
+ description = ''
+ Use this optional config directory instead of generating one from the
+ settings option.
+ '';
example = "/var/db/slapd.d";
};
+ # These options are deprecated
extraConfig = mkOption {
type = types.lines;
default = "";
@@ -164,10 +276,10 @@ in
";
example = literalExample ''
'''
- include ${openldap.out}/etc/schema/core.schema
- include ${openldap.out}/etc/schema/cosine.schema
- include ${openldap.out}/etc/schema/inetorgperson.schema
- include ${openldap.out}/etc/schema/nis.schema
+ include ${openldap}/etc/schema/core.schema
+ include ${openldap}/etc/schema/cosine.schema
+ include ${openldap}/etc/schema/inetorgperson.schema
+ include ${openldap}/etc/schema/nis.schema
database bdb
suffix dc=example,dc=org
@@ -244,57 +356,156 @@ in
};
meta = {
- maintainers = [ lib.maintainers.mic92 ];
+ maintainers = with lib.maintainters; [ mic92 kwohlfahrt ];
};
-
- ###### implementation
-
config = mkIf cfg.enable {
- assertions = [
- {
- assertion = cfg.configDir != null || cfg.rootpwFile != null || cfg.rootpw != null;
- message = "services.openldap: Unless configDir is set, either rootpw or rootpwFile must be set";
- }
- ];
+ warnings = let
+ deprecations = [
+ { old = "logLevel"; new = "attrs.olcLogLevel"; }
+ { old = "defaultSchemas";
+ new = "children.\"cn=schema\".includes";
+ newValue = "[\n ${lib.concatStringsSep "\n " [
+ "\${pkgs.openldap}/etc/schema/core.ldif"
+ "\${pkgs.openldap}/etc/schema/cosine.ldif"
+ "\${pkgs.openldap}/etc/schema/inetorgperson.ldif"
+ "\${pkgs.openldap}/etc/schema/nis.ldif"
+ ]}\n ]"; }
+ { old = "database"; new = "children.\"cn={1}${cfg.database}\""; newValue = "{ }"; }
+ { old = "suffix"; new = "children.\"cn={1}${cfg.database}\".attrs.olcSuffix"; }
+ { old = "dataDir"; new = "children.\"cn={1}${cfg.database}\".attrs.olcDbDirectory"; }
+ { old = "rootdn"; new = "children.\"cn={1}${cfg.database}\".attrs.olcRootDN"; }
+ { old = "rootpw"; new = "children.\"cn={1}${cfg.database}\".attrs.olcRootPW"; }
+ { old = "rootpwFile";
+ new = "children.\"cn={1}${cfg.database}\".attrs.olcRootPW";
+ newValue = "{ path = \"${cfg.rootpwFile}\"; }";
+ note = "The file should contain only the password (without \"rootpw \" as before)"; }
+ ];
+ in (optional (cfg.extraConfig != "" || cfg.extraDatabaseConfig != "") ''
+ The options `extraConfig` and `extraDatabaseConfig` of `services.openldap`
+ are deprecated. This is due to the deprecation of `slapd.conf`
+ upstream. Please migrate to `services.openldap.settings`.
+
+ After deploying this configuration, you can run:
+ slapcat -F ${configDir} -n0 -H 'ldap:///???(!(objectClass=olcSchemaConfig))'
+ on the same host to print your current configuration in LDIF format,
+ which should be straightforward to convert into Nix settings.
+ '') ++ (flatten (map (args@{old, new, ...}: lib.optional ((lib.hasAttr old cfg) && (lib.getAttr old cfg) != null) ''
+ The attribute `services.openldap.${old}` is deprecated. Please set it to
+ `null` and use the following option instead:
+
+ services.openldap.settings.${new} = ${args.newValue or (
+ let oldValue = (getAttr old cfg);
+ in if (isList oldValue) then "[ ${concatStringsSep " " oldValue} ]" else oldValue
+ )}
+ '') deprecations)) ++ (optional (cfg.configDir != null && (versionOlder config.system.stateVersion "20.09")) ''
+ The attribute `services.openldap.settings` now exists, and may be more
+ useful than `services.openldap.configDir`. If you continue to use
+ `configDir`, ensure that `olcPidFile` is set to "/run/slapd/slapd.pid".
+
+ Set `system.stateVersion` to "20.09" or greater to silence this message.
+ '');
+
+ assertions = [{
+ assertion = !(cfg.rootpwFile != null && cfg.rootpw != null);
+ message = "services.openldap: at most one of rootpw or rootpwFile must be set";
+ }];
environment.systemPackages = [ openldap ];
+ # Literal attributes must always be set (even if other top-level attributres are deprecated)
+ services.openldap.settings = {
+ attrs = {
+ objectClass = "olcGlobal";
+ cn = "config";
+ olcPidFile = "/run/slapd/slapd.pid";
+ } // (lib.optionalAttrs (cfg.logLevel != null) {
+ olcLogLevel = cfg.logLevel;
+ });
+ children = {
+ "cn=schema" = {
+ attrs = {
+ cn = "schema";
+ objectClass = "olcSchemaConfig";
+ };
+ includes = lib.optionals (cfg.defaultSchemas != null && cfg.defaultSchemas) [
+ "${openldap}/etc/schema/core.ldif"
+ "${openldap}/etc/schema/cosine.ldif"
+ "${openldap}/etc/schema/inetorgperson.ldif"
+ "${openldap}/etc/schema/nis.ldif"
+ ];
+ };
+ } // (lib.optionalAttrs (cfg.database != null) {
+ "olcDatabase={1}${cfg.database}".attrs = {
+ # objectClass is case-insensitive, so don't need to capitalize ${database}
+ objectClass = [ "olcdatabaseconfig" "olc${cfg.database}config" ];
+ olcDatabase = "{1}${cfg.database}";
+ } // (lib.optionalAttrs (cfg.suffix != null) {
+ olcSuffix = cfg.suffix;
+ }) // (lib.optionalAttrs (cfg.dataDir != null) {
+ olcDbDirectory = cfg.dataDir;
+ }) // (lib.optionalAttrs (cfg.rootdn != null) {
+ olcRootDN = cfg.rootdn; # TODO: Optional
+ }) // (lib.optionalAttrs (cfg.rootpw != null || cfg.rootpwFile != null) {
+ olcRootPW = (if cfg.rootpwFile != null then { path = cfg.rootpwFile; } else cfg.rootpw); # TODO: Optional
+ });
+ });
+ };
+
systemd.services.openldap = {
description = "LDAP server";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
- preStart = ''
+ preStart = let
+ dbSettings = lib.filterAttrs (name: value: lib.hasPrefix "olcDatabase=" name) cfg.settings.children;
+ dataDirs = lib.mapAttrsToList (name: value: value.attrs.olcDbDirectory) dbSettings;
+ settingsFile = pkgs.writeText "config.ldif" (lib.concatStringsSep "\n" (attrsToLdif "cn=config" cfg.settings));
+ in ''
mkdir -p /run/slapd
chown -R "${cfg.user}:${cfg.group}" /run/slapd
- ${optionalString (cfg.declarativeContents != null) ''
- rm -Rf "${cfg.dataDir}"
- ''}
- mkdir -p "${cfg.dataDir}"
- ${optionalString (cfg.declarativeContents != null) ''
- ${openldap.out}/bin/slapadd ${configOpts} -l ${dataFile}
- ''}
- chown -R "${cfg.user}:${cfg.group}" "${cfg.dataDir}"
- ${openldap}/bin/slaptest ${configOpts}
+ mkdir -p '${configDir}' ${lib.escapeShellArgs dataDirs}
+ chown "${cfg.user}:${cfg.group}" '${configDir}' ${lib.escapeShellArgs dataDirs}
+
+ ${lib.optionalString (cfg.configDir == null) (
+ if (cfg.extraConfig != "" || cfg.extraDatabaseConfig != "") then ''
+ rm -Rf '${configDir}'/*
+ # -u disables config generation, so just ignore the return code
+ ${openldap}/bin/slaptest -f ${configFile} -F ${configDir} || true
+ '' else ''
+ rm -Rf '${configDir}'/*
+ ${openldap}/bin/slapadd -F ${configDir} -n0 -l ${settingsFile}
+ ''
+ )}
+ chown -R "${cfg.user}:${cfg.group}" '${configDir}'
+
+ ${optionalString (cfg.declarativeContents != null) ''
+ rm -Rf '${lib.head dataDirs}'/*
+ ${openldap}/bin/slapadd -F ${configDir} -n1 -l ${dataFile}
+ chown -R "${cfg.user}:${cfg.group}" ${lib.escapeShellArgs dataDirs}
+ ''}
+
+ ${openldap}/bin/slaptest -u -F ${configDir}
'';
- serviceConfig.ExecStart =
- "${openldap.out}/libexec/slapd -d '${cfg.logLevel}' " +
- "-u '${cfg.user}' -g '${cfg.group}' " +
- "-h '${concatStringsSep " " cfg.urlList}' " +
- "${configOpts}";
+ serviceConfig = {
+ ExecStart = lib.concatStringsSep " " [
+ "${openldap}/libexec/slapd"
+ "-u '${cfg.user}'"
+ "-g '${cfg.group}'"
+ "-h '${concatStringsSep " " cfg.urlList}'"
+ "-F ${configDir}"
+ ];
+ Type = "forking";
+ PIDFile = cfg.settings.attrs.olcPidFile;
+ };
};
- users.users.openldap =
- { name = cfg.user;
- group = cfg.group;
- uid = config.ids.uids.openldap;
- };
-
- users.groups.openldap =
- { name = cfg.group;
- gid = config.ids.gids.openldap;
- };
+ users.users = lib.optionalAttrs (cfg.user == "openldap") {
+ openldap = { group = cfg.group; };
+ };
+ users.groups = lib.optionalAttrs (cfg.group == "openldap") {
+ openldap = {};
+ };
};
}
diff --git a/nixos/tests/openldap.nix b/nixos/tests/openldap.nix
index f8321a2c522..33b7b7f6608 100644
--- a/nixos/tests/openldap.nix
+++ b/nixos/tests/openldap.nix
@@ -1,33 +1,146 @@
-import ./make-test-python.nix {
- name = "openldap";
-
- machine = { pkgs, ... }: {
- services.openldap = {
- enable = true;
- suffix = "dc=example";
- rootdn = "cn=root,dc=example";
- rootpw = "notapassword";
- database = "bdb";
- extraDatabaseConfig = ''
- directory /var/db/openldap
- '';
- declarativeContents = ''
- dn: dc=example
- objectClass: domain
- dc: example
-
- dn: ou=users,dc=example
- objectClass: organizationalUnit
- ou: users
- '';
- };
- };
+{ pkgs, system ? builtins.currentSystem, ... }: let
+ declarativeContents = ''
+ dn: dc=example
+ objectClass: domain
+ dc: example
+ dn: ou=users,dc=example
+ objectClass: organizationalUnit
+ ou: users
+ '';
testScript = ''
machine.wait_for_unit("openldap.service")
machine.succeed(
- "systemctl status openldap.service",
'ldapsearch -LLL -D "cn=root,dc=example" -w notapassword -b "dc=example"',
)
'';
+in {
+ # New-style configuration
+ current = import ./make-test-python.nix {
+ inherit testScript;
+ name = "openldap";
+
+ machine = { pkgs, ... }: {
+ services.openldap = {
+ inherit declarativeContents;
+ enable = true;
+ defaultSchemas = null;
+ dataDir = null;
+ database = null;
+ settings = {
+ children = {
+ "cn=schema" = {
+ includes = [
+ "${pkgs.openldap}/etc/schema/core.ldif"
+ "${pkgs.openldap}/etc/schema/cosine.ldif"
+ "${pkgs.openldap}/etc/schema/inetorgperson.ldif"
+ "${pkgs.openldap}/etc/schema/nis.ldif"
+ ];
+ };
+ "olcDatabase={1}mdb" = {
+ attrs = {
+ objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ];
+ olcDatabase = "{1}mdb";
+ olcDbDirectory = "/var/db/openldap";
+ olcSuffix = "dc=example";
+ olcRootDN = "cn=root,dc=example";
+ olcRootPW = "notapassword";
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+
+ # Old-style configuration
+ shortOptions = import ./make-test-python.nix {
+ inherit testScript;
+ name = "openldap";
+
+ machine = { pkgs, ... }: {
+ services.openldap = {
+ inherit declarativeContents;
+ enable = true;
+ suffix = "dc=example";
+ rootdn = "cn=root,dc=example";
+ rootpw = "notapassword";
+ };
+ };
+ };
+
+ # Manually managed configDir, for example if dynamic config is essential
+ manualConfigDir = import ./make-test-python.nix {
+ name = "openldap";
+
+ machine = { pkgs, ... }: {
+ services.openldap = {
+ enable = true;
+ configDir = "/var/db/slapd.d";
+ # Silence warnings
+ defaultSchemas = null;
+ dataDir = null;
+ database = null;
+ };
+ };
+
+ testScript = let
+ contents = pkgs.writeText "data.ldif" declarativeContents;
+ config = pkgs.writeText "config.ldif" ''
+ dn: cn=config
+ cn: config
+ objectClass: olcGlobal
+ olcLogLevel: stats
+ olcPidFile: /run/slapd/slapd.pid
+
+ dn: cn=schema,cn=config
+ cn: schema
+ objectClass: olcSchemaConfig
+
+ include: file://${pkgs.openldap}/etc/schema/core.ldif
+ include: file://${pkgs.openldap}/etc/schema/cosine.ldif
+ include: file://${pkgs.openldap}/etc/schema/inetorgperson.ldif
+
+ dn: olcDatabase={1}mdb,cn=config
+ objectClass: olcDatabaseConfig
+ objectClass: olcMdbConfig
+ olcDatabase: {1}mdb
+ olcDbDirectory: /var/db/openldap
+ olcDbIndex: objectClass eq
+ olcSuffix: dc=example
+ olcRootDN: cn=root,dc=example
+ olcRootPW: notapassword
+ '';
+ in ''
+ machine.succeed(
+ "mkdir -p /var/db/slapd.d /var/db/openldap",
+ "slapadd -F /var/db/slapd.d -n0 -l ${config}",
+ "slapadd -F /var/db/slapd.d -n1 -l ${contents}",
+ "chown -R openldap:openldap /var/db/slapd.d /var/db/openldap",
+ "systemctl restart openldap",
+ )
+ '' + testScript;
+ };
+
+ # extraConfig forces use of slapd.conf, test this until that option is removed
+ legacyConfig = import ./make-test-python.nix {
+ inherit testScript;
+ name = "openldap";
+
+ machine = { pkgs, ... }: {
+ services.openldap = {
+ inherit declarativeContents;
+ enable = true;
+ suffix = "dc=example";
+ rootdn = "cn=root,dc=example";
+ rootpw = "notapassword";
+ extraConfig = ''
+ # No-op
+ '';
+ extraDatabaseConfig = ''
+ # No-op
+ '';
+ };
+ };
+ };
}