Merge pull request #179597 from Mic92/openldap-path

[staging] openldap: remove deprecated options, improve encapsulation
This commit is contained in:
Martin Weinelt 2022-07-22 00:26:32 +02:00 committed by GitHub
commit 457d109dcd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 220 additions and 196 deletions

View file

@ -3,7 +3,6 @@
with lib; with lib;
let let
cfg = config.services.openldap; cfg = config.services.openldap;
legacyOptions = [ "rootpwFile" "suffix" "dataDir" "rootdn" "rootpw" ];
openldap = cfg.package; openldap = cfg.package;
configDir = if cfg.configDir != null then cfg.configDir else "/etc/openldap/slapd.d"; configDir = if cfg.configDir != null then cfg.configDir else "/etc/openldap/slapd.d";
@ -11,7 +10,15 @@ let
# Can't do types.either with multiple non-overlapping submodules, so define our own # Can't do types.either with multiple non-overlapping submodules, so define our own
singleLdapValueType = lib.mkOptionType rec { singleLdapValueType = lib.mkOptionType rec {
name = "LDAP"; name = "LDAP";
description = "LDAP value"; # TODO: It would be nice to define a { secret = ...; } option, using
# systemd's LoadCredentials for secrets. That would remove the last
# barrier to using DynamicUser for openldap. This is blocked on
# systemd/systemd#19604
description = ''
LDAP value - either a string, or an attrset containing
<literal>path</literal> or <literal>base64</literal> for included
values or base-64 encoded values respectively.
'';
check = x: lib.isString x || (lib.isAttrs x && (x ? path || x ? base64)); check = x: lib.isString x || (lib.isAttrs x && (x ? path || x ? base64));
merge = lib.mergeEqualOption; merge = lib.mergeEqualOption;
}; };
@ -76,52 +83,12 @@ let
lib.flatten (lib.mapAttrsToList (name: value: attrsToLdif "${name},${dn}" value) children) lib.flatten (lib.mapAttrsToList (name: value: attrsToLdif "${name},${dn}" value) children)
); );
in { in {
imports = let
deprecationNote = "This option is removed due to the deprecation of `slapd.conf` upstream. Please migrate to `services.openldap.settings`, see the release notes for advice with this process.";
mkDatabaseOption = old: new:
lib.mkChangedOptionModule [ "services" "openldap" old ] [ "services" "openldap" "settings" "children" ]
(config: let
database = lib.getAttrFromPath [ "services" "openldap" "database" ] config;
value = lib.getAttrFromPath [ "services" "openldap" old ] config;
in lib.setAttrByPath ([ "olcDatabase={1}${database}" "attrs" ] ++ new) value);
in [
(lib.mkRemovedOptionModule [ "services" "openldap" "extraConfig" ] deprecationNote)
(lib.mkRemovedOptionModule [ "services" "openldap" "extraDatabaseConfig" ] deprecationNote)
(lib.mkChangedOptionModule [ "services" "openldap" "logLevel" ] [ "services" "openldap" "settings" "attrs" "olcLogLevel" ]
(config: lib.splitString " " (lib.getAttrFromPath [ "services" "openldap" "logLevel" ] config)))
(lib.mkChangedOptionModule [ "services" "openldap" "defaultSchemas" ] [ "services" "openldap" "settings" "children" "cn=schema" "includes"]
(config: lib.optionals (lib.getAttrFromPath [ "services" "openldap" "defaultSchemas" ] config) (
map (schema: "${openldap}/etc/schema/${schema}.ldif") [ "core" "cosine" "inetorgperson" "nis" ])))
(lib.mkChangedOptionModule [ "services" "openldap" "database" ] [ "services" "openldap" "settings" "children" ]
(config: let
database = lib.getAttrFromPath [ "services" "openldap" "database" ] config;
in {
"olcDatabase={1}${database}".attrs = {
# objectClass is case-insensitive, so don't need to capitalize ${database}
objectClass = [ "olcdatabaseconfig" "olc${database}config" ];
olcDatabase = "{1}${database}";
olcDbDirectory = lib.mkDefault "/var/db/openldap";
};
"cn=schema".includes = lib.mkDefault (
map (schema: "${openldap}/etc/schema/${schema}.ldif") [ "core" "cosine" "inetorgperson" "nis" ]
);
}))
(mkDatabaseOption "rootpwFile" [ "olcRootPW" "path" ])
(mkDatabaseOption "suffix" [ "olcSuffix" ])
(mkDatabaseOption "dataDir" [ "olcDbDirectory" ])
(mkDatabaseOption "rootdn" [ "olcRootDN" ])
(mkDatabaseOption "rootpw" [ "olcRootPW" ])
];
options = { options = {
services.openldap = { services.openldap = {
enable = mkOption { enable = mkOption {
type = types.bool; type = types.bool;
default = false; default = false;
description = " description = "Whether to enable the ldap server.";
Whether to enable the ldap server.
";
}; };
package = mkOption { package = mkOption {
@ -186,7 +153,7 @@ in {
attrs = { attrs = {
objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ]; objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ];
olcDatabase = "{1}mdb"; olcDatabase = "{1}mdb";
olcDbDirectory = "/var/db/ldap"; olcDbDirectory = "/var/lib/openldap/ldap";
olcDbIndex = [ olcDbIndex = [
"objectClass eq" "objectClass eq"
"cn pres,eq" "cn pres,eq"
@ -208,10 +175,20 @@ in {
default = null; default = null;
description = '' description = ''
Use this config directory instead of generating one from the Use this config directory instead of generating one from the
<literal>settings</literal> option. Overrides all NixOS settings. If <literal>settings</literal> option. Overrides all NixOS settings.
you use this option,ensure `olcPidFile` is set to `/run/slapd/slapd.conf`. '';
example = "/var/lib/openldap/slapd.d";
};
mutableConfig = mkOption {
type = types.bool;
default = false;
description = ''
Whether to allow writable on-line configuration. If
<literal>true</literal>, the NixOS settings will only be used to
initialize the OpenLDAP configuration if it does not exist, and are
subsequently ignored.
''; '';
example = "/var/db/slapd.d";
}; };
declarativeContents = mkOption { declarativeContents = mkOption {
@ -225,6 +202,11 @@ in {
reboot of the server. Performance-wise the database and indexes are reboot of the server. Performance-wise the database and indexes are
rebuilt on each server startup, so this will slow down server startup, rebuilt on each server startup, so this will slow down server startup,
especially with large databases. especially with large databases.
Note that the root of the DB must be defined in
<code>services.openldap.settings</code> and the
<code>olcDbDirectory</code> must begin with
<literal>"/var/lib/openldap"</literal>.
''; '';
example = lib.literalExpression '' example = lib.literalExpression ''
{ {
@ -247,11 +229,54 @@ in {
meta.maintainers = with lib.maintainers; [ mic92 kwohlfahrt ]; meta.maintainers = with lib.maintainers; [ mic92 kwohlfahrt ];
config = mkIf cfg.enable { config = let
assertions = map (opt: { dbSettings = mapAttrs' (name: { attrs, ... }: nameValuePair attrs.olcSuffix attrs)
assertion = ((getAttr opt cfg) != "_mkMergedOptionModule") -> (cfg.database != "_mkMergedOptionModule"); (filterAttrs (name: { attrs, ... }: (hasPrefix "olcDatabase=" name) && attrs ? olcSuffix) cfg.settings.children);
message = "Legacy OpenLDAP option `services.openldap.${opt}` requires `services.openldap.database` (use value \"mdb\" if unsure)"; settingsFile = pkgs.writeText "config.ldif" (lib.concatStringsSep "\n" (attrsToLdif "cn=config" cfg.settings));
}) legacyOptions; writeConfig = pkgs.writeShellScript "openldap-config" ''
set -euo pipefail
${lib.optionalString (!cfg.mutableConfig) ''
chmod -R u+w ${configDir}
rm -rf ${configDir}/*
''}
if [ ! -e "${configDir}/cn=config.ldif" ]; then
${openldap}/bin/slapadd -F ${configDir} -bcn=config -l ${settingsFile}
fi
chmod -R ${if cfg.mutableConfig then "u+rw" else "u+r-w"} ${configDir}
'';
contentsFiles = mapAttrs (dn: ldif: pkgs.writeText "${dn}.ldif" ldif) cfg.declarativeContents;
writeContents = pkgs.writeShellScript "openldap-load" ''
set -euo pipefail
rm -rf $2/*
${openldap}/bin/slapadd -F ${configDir} -b $1 -l $3
'';
in mkIf cfg.enable {
assertions = [{
assertion = (cfg.declarativeContents != {}) -> cfg.configDir == null;
message = ''
Declarative DB contents (${attrNames cfg.declarativeContents}) are not
supported with user-managed configuration.
'';
}] ++ (map (dn: {
assertion = (getAttr dn dbSettings) ? "olcDbDirectory";
# olcDbDirectory is necessary to prepopulate database using `slapadd`.
message = ''
Declarative DB ${dn} does not exist in `services.openldap.settings`, or does not have
`olcDbDirectory` configured.
'';
}) (attrNames cfg.declarativeContents)) ++ (mapAttrsToList (dn: { olcDbDirectory ? null, ... }: {
# For forward compatibility with `DynamicUser`, and to avoid accidentally clobbering
# directories with `declarativeContents`.
assertion = (olcDbDirectory != null) ->
((hasPrefix "/var/lib/openldap/" olcDbDirectory) && (olcDbDirectory != "/var/lib/openldap/"));
message = ''
Database ${dn} has `olcDbDirectory` (${olcDbDirectory}) that is not a subdirectory of
`/var/lib/openldap/`.
'';
}) dbSettings);
environment.systemPackages = [ openldap ]; environment.systemPackages = [ openldap ];
# Literal attributes must always be set # Literal attributes must always be set
@ -259,7 +284,6 @@ in {
attrs = { attrs = {
objectClass = "olcGlobal"; objectClass = "olcGlobal";
cn = "config"; cn = "config";
olcPidFile = "/run/slapd/slapd.pid";
}; };
children."cn=schema".attrs = { children."cn=schema".attrs = {
cn = "schema"; cn = "schema";
@ -276,44 +300,31 @@ in {
]; ];
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" ]; after = [ "network-online.target" ];
preStart = let
settingsFile = pkgs.writeText "config.ldif" (lib.concatStringsSep "\n" (attrsToLdif "cn=config" cfg.settings));
dbSettings = lib.filterAttrs (name: value: lib.hasPrefix "olcDatabase=" name) cfg.settings.children;
dataDirs = lib.mapAttrs' (name: value: lib.nameValuePair value.attrs.olcSuffix value.attrs.olcDbDirectory)
(lib.filterAttrs (_: value: value.attrs ? olcDbDirectory) dbSettings);
dataFiles = lib.mapAttrs (dn: contents: pkgs.writeText "${dn}.ldif" contents) cfg.declarativeContents;
mkLoadScript = dn: let
dataDir = lib.escapeShellArg (getAttr dn dataDirs);
in ''
rm -rf ${dataDir}/*
${openldap}/bin/slapadd -F ${lib.escapeShellArg configDir} -b ${dn} -l ${getAttr dn dataFiles}
chown -R "${cfg.user}:${cfg.group}" ${dataDir}
'';
in ''
mkdir -p /run/slapd
chown -R "${cfg.user}:${cfg.group}" /run/slapd
mkdir -p ${lib.escapeShellArg configDir} ${lib.escapeShellArgs (lib.attrValues dataDirs)}
chown "${cfg.user}:${cfg.group}" ${lib.escapeShellArg configDir} ${lib.escapeShellArgs (lib.attrValues dataDirs)}
${lib.optionalString (cfg.configDir == null) (''
rm -Rf ${configDir}/*
${openldap}/bin/slapadd -F ${configDir} -bcn=config -l ${settingsFile}
'')}
chown -R "${cfg.user}:${cfg.group}" ${lib.escapeShellArg configDir}
${lib.concatStrings (map mkLoadScript (lib.attrNames cfg.declarativeContents))}
${openldap}/bin/slaptest -u -F ${lib.escapeShellArg configDir}
'';
serviceConfig = { serviceConfig = {
User = cfg.user;
Group = cfg.group;
ExecStartPre = [
"!${pkgs.coreutils}/bin/mkdir -p ${configDir}"
"+${pkgs.coreutils}/bin/chown $USER ${configDir}"
] ++ (lib.optional (cfg.configDir == null) writeConfig)
++ (mapAttrsToList (dn: content: lib.escapeShellArgs [
writeContents dn (getAttr dn dbSettings).olcDbDirectory content
]) contentsFiles)
++ [ "${openldap}/bin/slaptest -u -F ${configDir}" ];
ExecStart = lib.escapeShellArgs ([ ExecStart = lib.escapeShellArgs ([
"${openldap}/libexec/slapd" "-u" cfg.user "-g" cfg.group "-F" configDir "${openldap}/libexec/slapd" "-d" "0" "-F" configDir "-h" (lib.concatStringsSep " " cfg.urlList)
"-h" (lib.concatStringsSep " " cfg.urlList)
]); ]);
Type = "notify"; Type = "notify";
# Fixes an error where openldap attempts to notify from a thread
# outside the main process:
# Got notification message from PID 6378, but reception only permitted for main PID 6377
NotifyAccess = "all"; NotifyAccess = "all";
PIDFile = cfg.settings.attrs.olcPidFile; RuntimeDirectory = "openldap";
StateDirectory = ["openldap"]
++ (map ({olcDbDirectory, ... }: removePrefix "/var/lib/" olcDbDirectory) (attrValues dbSettings));
StateDirectoryMode = "700";
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
}; };
}; };

View file

@ -1,9 +1,4 @@
{ pkgs ? (import ../.. { inherit system; config = { }; }) import ./make-test-python.nix ({ pkgs, ... }: let
, system ? builtins.currentSystem
, ...
}:
let
dbContents = '' dbContents = ''
dn: dc=example dn: dc=example
objectClass: domain objectClass: domain
@ -13,118 +8,136 @@ let
objectClass: organizationalUnit objectClass: organizationalUnit
ou: users ou: users
''; '';
testScript = ''
machine.wait_for_unit("openldap.service") ldifConfig = ''
machine.succeed( dn: cn=config
'ldapsearch -LLL -D "cn=root,dc=example" -w notapassword -b "dc=example"', cn: config
) objectClass: olcGlobal
olcLogLevel: stats
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 { in {
# New-style configuration name = "openldap";
current = import ./make-test-python.nix ({ pkgs, ... }: {
inherit testScript;
name = "openldap";
nodes.machine = { pkgs, ... }: { nodes.machine = { pkgs, ... }: {
environment.etc."openldap/root_password".text = "notapassword"; environment.etc."openldap/root_password".text = "notapassword";
services.openldap = { services.openldap = {
enable = true; enable = true;
settings = { urlList = [ "ldapi:///" "ldap://" ];
children = { settings = {
"cn=schema".includes = [ children = {
"${pkgs.openldap}/etc/schema/core.ldif" "cn=schema".includes = [
"${pkgs.openldap}/etc/schema/cosine.ldif" "${pkgs.openldap}/etc/schema/core.ldif"
"${pkgs.openldap}/etc/schema/inetorgperson.ldif" "${pkgs.openldap}/etc/schema/cosine.ldif"
"${pkgs.openldap}/etc/schema/nis.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 "olcDatabase={0}config" = {
attrs = { attrs = {
objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ]; objectClass = [ "olcDatabaseConfig" ];
olcDatabase = "{1}mdb"; olcDatabase = "{0}config";
olcDbDirectory = "/var/db/openldap"; olcRootDN = "cn=root,cn=config";
olcSuffix = "dc=example"; olcRootPW = "configpassword";
olcRootDN = { };
# cn=root,dc=example };
base64 = "Y249cm9vdCxkYz1leGFtcGxl"; "olcDatabase={1}mdb" = {
}; # This tests string, base64 and path values, as well as lists of string values
olcRootPW = { attrs = {
path = "/etc/openldap/root_password"; objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ];
}; olcDatabase = "{1}mdb";
olcDbDirectory = "/var/lib/openldap/db";
olcSuffix = "dc=example";
olcRootDN = {
# cn=root,dc=example
base64 = "Y249cm9vdCxkYz1leGFtcGxl";
};
olcRootPW = {
path = "/etc/openldap/root_password";
}; };
}; };
}; };
}; };
declarativeContents."dc=example" = dbContents;
};
};
}) { inherit pkgs system; };
# Old-style configuration
oldOptions = import ./make-test-python.nix ({ pkgs, ... }: {
inherit testScript;
name = "openldap";
nodes.machine = { pkgs, ... }: {
services.openldap = {
enable = true;
logLevel = "stats acl";
defaultSchemas = true;
database = "mdb";
suffix = "dc=example";
rootdn = "cn=root,dc=example";
rootpw = "notapassword";
declarativeContents."dc=example" = dbContents;
};
};
}) { inherit system pkgs; };
# Manually managed configDir, for example if dynamic config is essential
manualConfigDir = import ./make-test-python.nix ({ pkgs, ... }: {
name = "openldap";
nodes.machine = { pkgs, ... }: {
services.openldap = {
enable = true;
configDir = "/var/db/slapd.d";
}; };
}; };
testScript = let specialisation = {
contents = pkgs.writeText "data.ldif" dbContents; declarativeContents.configuration = { ... }: {
config = pkgs.writeText "config.ldif" '' services.openldap.declarativeContents."dc=example" = dbContents;
dn: cn=config };
cn: config mutableConfig.configuration = { ... }: {
objectClass: olcGlobal services.openldap = {
olcLogLevel: stats declarativeContents."dc=example" = dbContents;
olcPidFile: /run/slapd/slapd.pid mutableConfig = true;
};
};
manualConfigDir = {
inheritParentConfig = false;
configuration = { ... }: {
services.openldap = {
enable = true;
configDir = "/var/db/slapd.d";
};
};
};
};
};
testScript = { nodes, ... }: let
specializations = "${nodes.machine.config.system.build.toplevel}/specialisation";
changeRootPw = ''
dn: olcDatabase={1}mdb,cn=config
changetype: modify
replace: olcRootPW
olcRootPW: foobar
'';
in ''
# Test startup with empty DB
machine.wait_for_unit("openldap.service")
dn: cn=schema,cn=config with subtest("declarative contents"):
cn: schema machine.succeed('${specializations}/declarativeContents/bin/switch-to-configuration test')
objectClass: olcSchemaConfig 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 ${pkgs.writeText "rootpw.ldif" changeRootPw}')
include: file://${pkgs.openldap}/etc/schema/core.ldif with subtest("mutable config"):
include: file://${pkgs.openldap}/etc/schema/cosine.ldif machine.succeed('${specializations}/mutableConfig/bin/switch-to-configuration test')
include: file://${pkgs.openldap}/etc/schema/inetorgperson.ldif 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 ${pkgs.writeText "rootpw.ldif" changeRootPw}')
machine.succeed('ldapsearch -LLL -D "cn=root,dc=example" -w foobar -b "dc=example"')
dn: olcDatabase={1}mdb,cn=config with subtest("manual config dir"):
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( machine.succeed(
"mkdir -p /var/db/slapd.d /var/db/openldap", 'mkdir /var/db/slapd.d /var/db/openldap',
"slapadd -F /var/db/slapd.d -n0 -l ${config}", 'slapadd -F /var/db/slapd.d -n0 -l ${pkgs.writeText "config.ldif" ldifConfig}',
"slapadd -F /var/db/slapd.d -n1 -l ${contents}", 'slapadd -F /var/db/slapd.d -n1 -l ${pkgs.writeText "contents.ldif" dbContents}',
"chown -R openldap:openldap /var/db/slapd.d /var/db/openldap", 'chown -R openldap:openldap /var/db/slapd.d /var/db/openldap',
"systemctl restart openldap", '${specializations}/manualConfigDir/bin/switch-to-configuration test',
) )
'' + testScript; machine.succeed('ldapsearch -LLL -D "cn=root,dc=example" -w notapassword -b "dc=example"')
}) { inherit system pkgs; }; machine.succeed('ldapmodify -D cn=root,cn=config -w configpassword -f ${pkgs.writeText "rootpw.ldif" changeRootPw}')
} machine.succeed('ldapsearch -LLL -D "cn=root,dc=example" -w foobar -b "dc=example"')
'';
})

View file

@ -93,18 +93,18 @@ stdenv.mkDerivation rec {
"ac_cv_func_memcmp_working=yes" "ac_cv_func_memcmp_working=yes"
] ++ lib.optional stdenv.isFreeBSD "--with-pic"; ] ++ lib.optional stdenv.isFreeBSD "--with-pic";
makeFlags = [ NIX_CFLAGS_COMPILE = [ "-DLDAPI_SOCK=\"/run/openldap/ldapi\"" ];
makeFlags= [
"CC=${stdenv.cc.targetPrefix}cc" "CC=${stdenv.cc.targetPrefix}cc"
"STRIP=" # Disable install stripping as it breaks cross-compiling. We strip binaries anyway in fixupPhase. "STRIP=" # Disable install stripping as it breaks cross-compiling. We strip binaries anyway in fixupPhase.
"STRIP_OPTS="
"prefix=${placeholder "out"}" "prefix=${placeholder "out"}"
"sysconfdir=${placeholder "out"}/etc" "sysconfdir=${placeholder "out"}/etc"
"systemdsystemunitdir=${placeholder "out"}/lib/systemd/system" "systemdsystemunitdir=${placeholder "out"}/lib/systemd/system"
# contrib modules require these # contrib modules require these
"moduledir=${placeholder "out"}/lib/modules" "moduledir=${placeholder "out"}/lib/modules"
"mandir=${placeholder "out"}/share/man" "mandir=${placeholder "out"}/share/man"
] ++ lib.optionals (stdenv.buildPlatform != stdenv.hostPlatform) [
# Can be unconditional, doing it like this to prevent a mass rebuild.
"STRIP_OPTS="
]; ];
extraContribModules = [ extraContribModules = [