Skip to content
92 changes: 61 additions & 31 deletions nixos/modules/services/databases/openldap.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down Expand Up @@ -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 = {};
Expand Down Expand Up @@ -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
Expand All @@ -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" ];
Expand Down
187 changes: 105 additions & 82 deletions nixos/tests/openldap.nix
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
{ pkgs ? (import ../.. { inherit system; config = { }; })
, system ? builtins.currentSystem
, ...
}:
import ./make-test-python.nix ({ pkgs, ... }:

let
dbContents = ''
Expand All @@ -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"')
'';
})
13 changes: 13 additions & 0 deletions pkgs/development/libraries/openldap/default-socket-path.patch
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions pkgs/development/libraries/openldap/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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" ];

Expand Down