Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 37 additions & 64 deletions nixos/modules/services/databases/openldap.nix
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ let
cfg = config.services.openldap;
legacyOptions = [ "rootpwFile" "suffix" "dataDir" "rootdn" "rootpw" ];
openldap = cfg.package;
configDir = if cfg.configDir != null then cfg.configDir else "/etc/openldap/slapd.d";
useDefaultConfDir = cfg.configDir == null;
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);

ldapValueType = let
# Can't do types.either with multiple non-overlapping submodules, so define our own
Expand Down Expand Up @@ -76,44 +83,6 @@ let
lib.flatten (lib.mapAttrsToList (name: value: attrsToLdif "${name},${dn}" value) children)
);
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 = {
services.openldap = {
enable = mkOption {
Expand Down Expand Up @@ -186,7 +155,7 @@ in {
attrs = {
objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ];
olcDatabase = "{1}mdb";
olcDbDirectory = "/var/db/ldap";
olcDbDirectory = "/var/lib/openldap/db1";
olcDbIndex = [
"objectClass eq"
"cn pres,eq"
Expand All @@ -208,10 +177,9 @@ in {
default = null;
description = ''
Use this config directory instead of generating one from the
<literal>settings</literal> option. Overrides all NixOS settings. If
you use this option,ensure `olcPidFile` is set to `/run/slapd/slapd.conf`.
<literal>settings</literal> option. Overrides all NixOS settings.
'';
example = "/var/db/slapd.d";
example = "/var/lib/openldap/slapd.d";
};

declarativeContents = mkOption {
Expand All @@ -225,6 +193,10 @@ in {
reboot of the server. Performance-wise the database and indexes are
rebuilt on each server startup, so this will slow down server startup,
especially with large databases.

Note that the DIT root of the declarative DB must be defined in
<code>services.openldap.settings</code> AND the <code>olcDbDirectory</code>
must be prefixed by "/var/lib/openldap/"
'';
example = lib.literalExpression ''
{
Expand All @@ -251,15 +223,21 @@ in {
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;
}) legacyOptions ++ map (dn: {
assertion = dataDirs ? "${dn}";
message = ''
declarative DB ${dn} does not exist in "servies.openldap.settings" or it exists but the "olcDbDirectory"
is not prefixed by "/var/lib/openldap/"
'';
}) declarativeDNs;
environment.systemPackages = [ openldap ];

# Literal attributes must always be set
services.openldap.settings = {
attrs = {
objectClass = "olcGlobal";
cn = "config";
olcPidFile = "/run/slapd/slapd.pid";
olcPidFile = "/run/openldap/slapd.pid";
};
children."cn=schema".attrs = {
cn = "schema";
Expand All @@ -273,40 +251,35 @@ in {
after = [ "network.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);
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}
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}/*
${lib.optionalString useDefaultConfDir ''
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))}
${lib.concatStrings (map mkLoadScript declarativeDNs)}
${openldap}/bin/slaptest -u -F ${lib.escapeShellArg configDir}
'';
serviceConfig = {
User = cfg.user;
Group = cfg.group;
Type = "forking";
ExecStart = lib.escapeShellArgs ([
"${openldap}/libexec/slapd" "-u" cfg.user "-g" cfg.group "-F" configDir
"${openldap}/libexec/slapd" "-F" configDir
"-h" (lib.concatStringsSep " " cfg.urlList)
]);
Type = "forking";
StateDirectory = [ "openldap/slapd.d" ] ++ additionalStateDirectories;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use ConfigurationDirectory for the configuration? IMO it makes sense for it to be separate from the rest of the state.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It depends... when an application manages its own configuration we often call that "state" in NixOS land.

This module allows either the application or NixOS to manage the configuration (and maybe both?)

One argument for using StateDirectory is that the ownership allows you to run the application as non root entirely - ConfigurationDirectory does not allow this.

Good points on both side.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, openldap is a bit weird with this. Because on the one hand, the OLC configuration can be changed at run-time, by writing to the appropriate directory via LDAP. OTOH, we nuke those settings every restart, with the declarative settings. This was a conscious choice, since it seems the most nix-y.

IMO it would be nice to deny writes to the configuration directory entirely, so that we don't surprise users by dropping settings after they are applied. I think this would require running the pre-start script as root, but the process could still be non-root (as it just needs read access)?

StateDirectoryMode = "700";
RuntimeDirectory = "openldap";
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
PIDFile = cfg.settings.attrs.olcPidFile;
};
};
Expand Down
23 changes: 2 additions & 21 deletions nixos/tests/openldap.nix
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ in {
attrs = {
objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ];
olcDatabase = "{1}mdb";
olcDbDirectory = "/var/db/openldap";
olcDbDirectory = "/var/lib/openldap/current";
olcSuffix = "dc=example";
olcRootDN = {
# cn=root,dc=example
Expand All @@ -60,25 +60,6 @@ in {
};
}) { inherit pkgs system; };

# Old-style configuration
oldOptions = import ./make-test-python.nix ({ pkgs, ... }: {
inherit testScript;
name = "openldap";

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";
Expand All @@ -97,7 +78,7 @@ in {
cn: config
objectClass: olcGlobal
olcLogLevel: stats
olcPidFile: /run/slapd/slapd.pid
olcPidFile: /run/openldap/slapd.pid

dn: cn=schema,cn=config
cn: schema
Expand Down