{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.openldap;
openldap = cfg.package;
configDir = if cfg.configDir != null then cfg.configDir else "/etc/openldap/slapd.d";
ldapValueType = let
singleLdapValueType = types.oneOf [
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.
'';
};
};
})
(types.submodule {
options = {
base64 = mkOption {
type = types.str;
description = ''
A base64-encoded LDAP attribute. Useful for storing values which
contain special characters (e.g. newlines) in LDIF files.
'';
};
};
})
];
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
listValues = if lib.isList values then values else lib.singleton values;
in map (value:
if lib.isAttrs value then
if lib.hasAttr "path" value
then "${attr}:< file://${value.path}"
else "${attr}:: ${value.base64}"
else "${attr}: ${lib.replaceStrings [ "\n" ] [ "\n " ] value}"
) listValues;
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 {
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.";
in [
(lib.mkRemovedOptionModule [ "services" "openldap" "extraConfig" ] deprecationNote)
(lib.mkRemovedOptionModule [ "services" "openldap" "extraDatabaseConfig" ] deprecationNote)
];
options = {
services.openldap = {
enable = mkOption {
type = types.bool;
default = false;
description = "
Whether to enable the ldap server.
";
};
package = mkOption {
type = types.package;
default = pkgs.openldap;
description = ''
OpenLDAP package to use.
This can be used to, for example, set an OpenLDAP package
with custom overrides to enable modules or other
functionality.
'';
};
user = mkOption {
type = types.str;
default = "openldap";
description = "User account under which slapd runs.";
};
group = mkOption {
type = types.str;
default = "openldap";
description = "Group account under which slapd runs.";
};
urlList = mkOption {
type = types.listOf types.str;
default = [ "ldap:///" ];
description = "URL list slapd should listen on.";
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.nullOr types.path;
default = "/var/db/openldap";
description = "The database directory.";
};
defaultSchemas = mkOption {
type = types.nullOr types.bool;
default = true;
description = ''
Include the default schemas core, cosine, inetorgperson and nis.
'';
};
database = mkOption {
type = types.nullOr types.str;
default = "mdb";
description = "Backend to use for the first database.";
};
suffix = mkOption {
type = types.nullOr types.str;
default = null;
example = "dc=example,dc=org";
description = ''
Specify the DN suffix of queries that will be passed to the first
backend database.
'';
};
rootdn = mkOption {
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.
'';
};
rootpw = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
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.
'';
};
rootpwFile = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Password file for the root user.
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.nullOr (types.coercedTo types.str (lib.splitString " ") (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 config directory instead of generating one from the
settings option. Overrides all NixOS settings. If
you use this option,ensure `olcPidFile` is set to `/run/slapd/slapd.conf`.
'';
example = "/var/db/slapd.d";
};
declarativeContents = mkOption {
type = with types; either lines (attrsOf lines);
default = {};
description = ''
Declarative contents for the first LDAP database, in LDIF format.
Note a few facts when using it. First, the database
must be stored in the directory defined by
dataDir
. Second, all dataDir
will be erased
when starting the LDAP server. Third, modifications to the database
are not prevented, they are just dropped on the next reboot of the
server. Finally, performance-wise the database and indexes are rebuilt
on each server startup, so this will slow down server startup,
especially with large databases.
'';
example = ''
dn: dc=example,dc=org
objectClass: domain
dc: example
dn: ou=users,dc=example,dc=org
objectClass = organizationalUnit
ou: users
# ...
'';
};
};
};
meta = {
maintainers = with lib.maintainters; [ mic92 kwohlfahrt ];
};
# TODO: Check that dataDir/declarativeContents/configDir all match
# - deprecate declarativeContents = ''...'';
# - no declarativeContents = ''...'' if dataDir == null;
# - no declarativeContents = { ... } if configDir != null
config = mkIf cfg.enable {
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 (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));
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 = let
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);
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
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}
${if types.lines.check cfg.declarativeContents
then (let
dataFile = pkgs.writeText "ldap-contents.ldif" cfg.declarativeContents;
in ''
rm -rf ${lib.escapeShellArg cfg.dataDir}/*
${openldap}/bin/slapadd -F ${lib.escapeShellArg configDir} -l ${dataFile}
chown -R "${cfg.user}:${cfg.group}" ${lib.escapeShellArg cfg.dataDir}
'')
else (let
dataFiles = lib.mapAttrs (dn: contents: pkgs.writeText "${dn}.ldif" contents) cfg.declarativeContents;
in ''
${lib.concatStrings (lib.mapAttrsToList (dn: file: let
dataDir = lib.escapeShellArg (getAttr dn dataDirs);
in ''
rm -rf ${dataDir}/*
${openldap}/bin/slapadd -F ${lib.escapeShellArg configDir} -b ${dn} -l ${file}
chown -R "${cfg.user}:${cfg.group}" ${dataDir}
'') dataFiles)}
'')
}
${openldap}/bin/slaptest -u -F ${lib.escapeShellArg configDir}
'';
serviceConfig = {
ExecStart = lib.escapeShellArgs ([
"${openldap}/libexec/slapd" "-u" cfg.user "-g" cfg.group "-F" configDir
"-h" (lib.concatStringsSep " " cfg.urlList)
]);
Type = "forking";
PIDFile = cfg.settings.attrs.olcPidFile;
};
};
users.users = lib.optionalAttrs (cfg.user == "openldap") {
openldap = {
group = cfg.group;
isSystemUser = true;
};
};
users.groups = lib.optionalAttrs (cfg.group == "openldap") {
openldap = {};
};
};
}