diff --git a/nixos/modules/services/databases/openldap.nix b/nixos/modules/services/databases/openldap.nix index c7880393f3d00..1b4a7f31e9770 100644 --- a/nixos/modules/services/databases/openldap.nix +++ b/nixos/modules/services/databases/openldap.nix @@ -3,22 +3,30 @@ with lib; let cfg = config.services.openldap; - legacyOptions = [ "rootpwFile" "suffix" "dataDir" "rootdn" "rootpw" ]; openldap = cfg.package; - useDefaultConfDir = cfg.configDir == null; + escapeSystemd = s: replaceStrings ["%"] ["%%"] s; configDir = if cfg.configDir != null then cfg.configDir else "/var/lib/openldap/slapd.d"; dbSettings = filterAttrs (name: value: hasPrefix "olcDatabase=" name) cfg.settings.children; dataDirs = mapAttrs' (_: value: nameValuePair value.attrs.olcSuffix (removePrefix "/var/lib/openldap/" value.attrs.olcDbDirectory)) (lib.filterAttrs (_: value: value.attrs ? olcDbDirectory && hasPrefix "/var/lib/openldap/" value.attrs.olcDbDirectory) dbSettings); - declarativeDNs = attrNames cfg.declarativeContents; additionalStateDirectories = map (sfx: "openldap/" + sfx) (attrValues dataDirs); + dataFiles = lib.mapAttrs (dn: contents: pkgs.writeText "${dn}.ldif" contents) cfg.declarativeContents; + declarativeDNs = attrNames cfg.declarativeContents; + ldapValueType = let # Can't do types.either with multiple non-overlapping submodules, so define our own singleLdapValueType = lib.mkOptionType rec { name = "LDAP"; - description = "LDAP value"; + # TODO: It might be worth defining a { secret = ...; } option, leveraging + # systemd's LoadCredentials for secrets. That should remove the last + # barrier to using DynamicUser for openldap. However, this is blocked on + # $CREDENTIALS_DIRECTORY being available in ExecStartPre. + description = '' + LDAP value - either a string, or an attrset containing `path` or + `base64`, for included values or base-64 encoded values respectively. + ''; check = x: lib.isString x || (lib.isAttrs x && (x ? path || x ? base64)); merge = lib.mergeEqualOption; }; @@ -182,6 +190,16 @@ in { example = "/var/lib/openldap/slapd.d"; }; + mutableConfig = mkOption { + type = types.bool; + default = false; + description = '' + Whether to allow writable on-line configuration. If `true`, the NixOS + settings will only be used to initialize the OpenLDAP configuration + if it does not exist, and are subsequently ignored. + ''; + }; + declarativeContents = mkOption { type = with types; attrsOf lines; default = {}; @@ -220,16 +238,23 @@ in { meta.maintainers = with lib.maintainers; [ mic92 kwohlfahrt ]; config = mkIf cfg.enable { - assertions = map (opt: { - assertion = ((getAttr opt cfg) != "_mkMergedOptionModule") -> (cfg.database != "_mkMergedOptionModule"); - message = "Legacy OpenLDAP option `services.openldap.${opt}` requires `services.openldap.database` (use value \"mdb\" if unsure)"; - }) legacyOptions ++ map (dn: { + assertions = [{ + assertion = (cfg.configDir != null) -> declarativeDNs == []; + message = '' + Declarative DB contents (${dn}) are not supported with user-managed configuration directory". + ''; + }] ++ (map (dn: { assertion = dataDirs ? "${dn}"; message = '' - declarative DB ${dn} does not exist in "servies.openldap.settings" or it exists but the "olcDbDirectory" + Declarative DB ${dn} does not exist in "services.openldap.settings" or it exists but the "olcDbDirectory" is not prefixed by "/var/lib/openldap/" ''; - }) declarativeDNs; + }) declarativeDNs) ++ (map (dir: { + assertion = !(hasPrefix "slapd.d" dir); + message = '' + Database path may not be "/var/lib/openldap/slapd.d", this path is used for configuration. + ''; + }) (attrValues dataDirs)); environment.systemPackages = [ openldap ]; # Literal attributes must always be set @@ -249,33 +274,38 @@ in { description = "LDAP server"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; - preStart = let - settingsFile = pkgs.writeText "config.ldif" (lib.concatStringsSep "\n" (attrsToLdif "cn=config" cfg.settings)); - dataFiles = lib.mapAttrs (dn: contents: pkgs.writeText "${dn}.ldif" contents) cfg.declarativeContents; - mkLoadScript = dn: let - dataDir = lib.escapeShellArg ("/var/lib/openldap/" + getAttr dn dataDirs); - in '' - rm -rf ${dataDir}/* - ${openldap}/bin/slapadd -F ${lib.escapeShellArg configDir} -b ${dn} -l ${getAttr dn dataFiles} + serviceConfig = let + # This cannot be built in a derivation, because it needs filesystem access for included files + writeConfig = let + settingsFile = pkgs.writeText "config.ldif" (lib.concatStringsSep "\n" (attrsToLdif "cn=config" cfg.settings)); + in pkgs.writeShellScript "openldap-config" '' + set -euo pipefail + + ${lib.optionalString (!cfg.mutableConfig) "rm -rf ${configDir}/*"} + if [ -z "$(ls -A ${configDir})" ]; then + ${openldap}/bin/slapadd -F ${configDir} -bcn=config -l ${settingsFile} + fi + chmod -R ${if cfg.mutableConfig then "u+rw" else "u+r-w"} ${configDir} ''; - in '' - ${lib.optionalString useDefaultConfDir '' - rm -rf ${configDir}/* - ${openldap}/bin/slapadd -F ${configDir} -bcn=config -l ${settingsFile} - ''} + writeContents = pkgs.writeShellScript "openldap-load" '' + set -euo pipefail - ${lib.concatStrings (map mkLoadScript declarativeDNs)} - ${openldap}/bin/slaptest -u -F ${lib.escapeShellArg configDir} - ''; - serviceConfig = { + rm -rf /var/lib/openldap/$2/* + ${openldap}/bin/slapadd -F ${configDir} -b $1 -l $3 + ''; + in { User = cfg.user; Group = cfg.group; Type = "forking"; - ExecStart = lib.escapeShellArgs ([ + ExecStartPre = + (lib.optional (cfg.configDir == null) writeConfig) + ++ (map (dn: lib.escapeShellArgs [writeContents dn (getAttr dn dataDirs) (getAttr dn dataFiles)]) declarativeDNs) + ++ [ "${openldap}/bin/slaptest -u -F ${configDir}" ]; + ExecStart = lib.escapeShellArgs [ "${openldap}/libexec/slapd" "-F" configDir - "-h" (lib.concatStringsSep " " cfg.urlList) - ]); - StateDirectory = [ "openldap/slapd.d" ] ++ additionalStateDirectories; + "-h" (escapeSystemd (lib.concatStringsSep " " cfg.urlList)) + ]; + StateDirectory = lib.optional (cfg.configDir == null) ([ "openldap/slapd.d" ] ++ additionalStateDirectories); StateDirectoryMode = "700"; RuntimeDirectory = "openldap"; AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; diff --git a/nixos/tests/openldap.nix b/nixos/tests/openldap.nix index b077838b4de56..aa017ebf21a71 100644 --- a/nixos/tests/openldap.nix +++ b/nixos/tests/openldap.nix @@ -1,7 +1,4 @@ -{ pkgs ? (import ../.. { inherit system; config = { }; }) -, system ? builtins.currentSystem -, ... -}: +import ./make-test-python.nix ({ pkgs, ... }: let dbContents = '' @@ -13,99 +10,125 @@ let objectClass: organizationalUnit ou: users ''; - testScript = '' - machine.wait_for_unit("openldap.service") - machine.succeed( - 'ldapsearch -LLL -D "cn=root,dc=example" -w notapassword -b "dc=example"', - ) + manualConfig = pkgs.writeText "config.ldif" '' + dn: cn=config + cn: config + objectClass: olcGlobal + olcLogLevel: stats + olcPidFile: /run/openldap/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={0}config,cn=config + olcDatabase: {0}config + objectClass: olcDatabaseConfig + olcRootDN: cn=root,cn=config + olcRootPW: configpassword + + 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 { - # New-style configuration - current = import ./make-test-python.nix ({ pkgs, ... }: { - inherit testScript; - name = "openldap"; + name = "openldap"; - machine = { pkgs, ... }: { - environment.etc."openldap/root_password".text = "notapassword"; - services.openldap = { - enable = true; - 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" = { - # This tests string, base64 and path values, as well as lists of string values - attrs = { - objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ]; - olcDatabase = "{1}mdb"; - olcDbDirectory = "/var/lib/openldap/current"; - olcSuffix = "dc=example"; - olcRootDN = { - # cn=root,dc=example - base64 = "Y249cm9vdCxkYz1leGFtcGxl"; - }; - olcRootPW = { - path = "/etc/openldap/root_password"; - }; + machine = { pkgs, ... }: { + environment.etc."openldap/root_password".text = "notapassword"; + services.openldap = { + enable = true; + urlList = [ "ldap:///" "ldapi:///" ]; + 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={0}config" = { + attrs = { + objectClass = "olcDatabaseConfig"; + olcDatabase = "{0}config"; + olcRootDN = "cn=root,cn=config"; + olcRootPW = "configpassword"; + }; + }; + "olcDatabase={1}mdb" = { + # This tests string, base64 and path values, as well as lists of string values + attrs = { + objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ]; + olcDatabase = "{1}mdb"; + olcDbDirectory = "/var/lib/openldap/current"; + olcSuffix = "dc=example"; + olcRootDN = { + # cn=root,dc=example + base64 = "Y249cm9vdCxkYz1leGFtcGxl"; + }; + olcRootPW = { + path = "/etc/openldap/root_password"; }; }; }; }; - declarativeContents."dc=example" = dbContents; }; + declarativeContents."dc=example" = dbContents; }; - }) { inherit pkgs system; }; - - # Manually managed configDir, for example if dynamic config is essential - manualConfigDir = import ./make-test-python.nix ({ pkgs, ... }: { - name = "openldap"; - - machine = { pkgs, ... }: { - services.openldap = { - enable = true; - configDir = "/var/db/slapd.d"; + specialisation = { + mutableConfig.configuration = { ... }: { + services.openldap.mutableConfig = true; + }; + manualConfigDir = { + inheritParentConfig = false; + configuration = { ... }: { + services.openldap.enable = true; + services.openldap.configDir = "/var/db/slapd.d"; + }; }; }; + }; - testScript = let - contents = pkgs.writeText "data.ldif" dbContents; - config = pkgs.writeText "config.ldif" '' - dn: cn=config - cn: config - objectClass: olcGlobal - olcLogLevel: stats - olcPidFile: /run/openldap/slapd.pid - - dn: cn=schema,cn=config - cn: schema - objectClass: olcSchemaConfig + testScript = { nodes, ... }: let + config = nodes.machine.config.system.build.toplevel; + changeRootPW = pkgs.writeText "changeRootPW.ldif" '' + dn: olcDatabase={1}mdb,cn=config + changetype: modify + replace: olcRootPW + olcRootPW: foobar + ''; + in '' + machine.wait_for_unit("openldap.service") + machine.succeed('ldapsearch -LLL -D "cn=root,dc=example" -w notapassword -b "dc=example"') + machine.fail("ldapmodify -D cn=root,cn=config -w configpassword -f ${changeRootPW}") - include: file://${pkgs.openldap}/etc/schema/core.ldif - include: file://${pkgs.openldap}/etc/schema/cosine.ldif - include: file://${pkgs.openldap}/etc/schema/inetorgperson.ldif + with subtest("handles mutable config"): + machine.succeed("${config}/specialisation/mutableConfig/bin/switch-to-configuration test") + machine.succeed('ldapsearch -LLL -D "cn=root,dc=example" -w notapassword -b "dc=example"') + machine.succeed("ldapmodify -D cn=root,cn=config -w configpassword -f ${changeRootPW}") + machine.systemctl('restart openldap') + machine.succeed('ldapsearch -LLL -D "cn=root,dc=example" -w foobar -b "dc=example"') - 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 '' + with subtest("handles manual config dir"): 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}", + "slapadd -F /var/db/slapd.d -n0 -l ${manualConfig}", + "slapadd -F /var/db/slapd.d -n1 -l ${pkgs.writeText "data.ldif" dbContents}", "chown -R openldap:openldap /var/db/slapd.d /var/db/openldap", - "systemctl restart openldap", + "${config}/specialisation/manualConfigDir/bin/switch-to-configuration test", ) - '' + testScript; - }) { inherit system pkgs; }; -} + machine.succeed('ldapsearch -LLL -D "cn=root,dc=example" -w notapassword -b "dc=example"') + machine.succeed("ldapmodify -D cn=root,cn=config -w configpassword -f ${changeRootPW}") + machine.succeed('ldapsearch -LLL -D "cn=root,dc=example" -w foobar -b "dc=example"') + ''; +}) diff --git a/pkgs/development/libraries/openldap/default-socket-path.patch b/pkgs/development/libraries/openldap/default-socket-path.patch new file mode 100644 index 0000000000000..0ae3bae35d529 --- /dev/null +++ b/pkgs/development/libraries/openldap/default-socket-path.patch @@ -0,0 +1,13 @@ +diff --git a/include/ldap_defaults.h b/include/ldap_defaults.h +index 916d6bc66..14e08724a 100644 +--- a/include/ldap_defaults.h ++++ b/include/ldap_defaults.h +@@ -39,7 +39,7 @@ + #define LDAP_ENV_PREFIX "LDAP" + + /* default ldapi:// socket */ +-#define LDAPI_SOCK LDAP_RUNDIR LDAP_DIRSEP "run" LDAP_DIRSEP "ldapi" ++#define LDAPI_SOCK LDAP_RUNDIR LDAP_DIRSEP "run" LDAP_DIRSEP "openldap" LDAP_DIRSEP "ldapi" + + /* + * SLAPD DEFINITIONS diff --git a/pkgs/development/libraries/openldap/default.nix b/pkgs/development/libraries/openldap/default.nix index f9e2b3c0b3fc7..be1e2e5a2bd84 100644 --- a/pkgs/development/libraries/openldap/default.nix +++ b/pkgs/development/libraries/openldap/default.nix @@ -12,6 +12,8 @@ stdenv.mkDerivation rec { sha256 = "sha256-V7WSVL4V0L9qmrPVFMHAV3ewISMpFTMTSofJRGj49Hs="; }; + patches = [ ./default-socket-path.patch ]; + # TODO: separate "out" and "bin" outputs = [ "out" "dev" "man" "devdoc" ];