1{ config, lib, pkgs, ... }:
2
3with lib;
4let
5 cfg = config.services.openldap;
6 legacyOptions = [ "rootpwFile" "suffix" "dataDir" "rootdn" "rootpw" ];
7 openldap = cfg.package;
8 configDir = if cfg.configDir != null then cfg.configDir else "/etc/openldap/slapd.d";
9
10 ldapValueType = let
11 # Can't do types.either with multiple non-overlapping submodules, so define our own
12 singleLdapValueType = lib.mkOptionType rec {
13 name = "LDAP";
14 description = "LDAP value";
15 check = x: lib.isString x || (lib.isAttrs x && (x ? path || x ? base64));
16 merge = lib.mergeEqualOption;
17 };
18 # We don't coerce to lists of single values, as some values must be unique
19 in types.either singleLdapValueType (types.listOf singleLdapValueType);
20
21 ldapAttrsType =
22 let
23 options = {
24 attrs = mkOption {
25 type = types.attrsOf ldapValueType;
26 default = {};
27 description = "Attributes of the parent entry.";
28 };
29 children = mkOption {
30 # Hide the child attributes, to avoid infinite recursion in e.g. documentation
31 # Actual Nix evaluation is lazy, so this is not an issue there
32 type = let
33 hiddenOptions = lib.mapAttrs (name: attr: attr // { visible = false; }) options;
34 in types.attrsOf (types.submodule { options = hiddenOptions; });
35 default = {};
36 description = "Child entries of the current entry, with recursively the same structure.";
37 example = lib.literalExample ''
38 {
39 "cn=schema" = {
40 # The attribute used in the DN must be defined
41 attrs = { cn = "schema"; };
42 children = {
43 # This entry's DN is expanded to "cn=foo,cn=schema"
44 "cn=foo" = { ... };
45 };
46 # These includes are inserted after "cn=schema", but before "cn=foo,cn=schema"
47 includes = [ ... ];
48 };
49 }
50 '';
51 };
52 includes = mkOption {
53 type = types.listOf types.path;
54 default = [];
55 description = ''
56 LDIF files to include after the parent's attributes but before its children.
57 '';
58 };
59 };
60 in types.submodule { inherit options; };
61
62 valueToLdif = attr: values: let
63 listValues = if lib.isList values then values else lib.singleton values;
64 in map (value:
65 if lib.isAttrs value then
66 if lib.hasAttr "path" value
67 then "${attr}:< file://${value.path}"
68 else "${attr}:: ${value.base64}"
69 else "${attr}: ${lib.replaceStrings [ "\n" ] [ "\n " ] value}"
70 ) listValues;
71
72 attrsToLdif = dn: { attrs, children, includes, ... }: [''
73 dn: ${dn}
74 ${lib.concatStringsSep "\n" (lib.flatten (lib.mapAttrsToList valueToLdif attrs))}
75 ''] ++ (map (path: "include: file://${path}\n") includes) ++ (
76 lib.flatten (lib.mapAttrsToList (name: value: attrsToLdif "${name},${dn}" value) children)
77 );
78in {
79 imports = let
80 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.";
81 mkDatabaseOption = old: new:
82 lib.mkChangedOptionModule [ "services" "openldap" old ] [ "services" "openldap" "settings" "children" ]
83 (config: let
84 database = lib.getAttrFromPath [ "services" "openldap" "database" ] config;
85 value = lib.getAttrFromPath [ "services" "openldap" old ] config;
86 in lib.setAttrByPath ([ "olcDatabase={1}${database}" "attrs" ] ++ new) value);
87 in [
88 (lib.mkRemovedOptionModule [ "services" "openldap" "extraConfig" ] deprecationNote)
89 (lib.mkRemovedOptionModule [ "services" "openldap" "extraDatabaseConfig" ] deprecationNote)
90
91 (lib.mkChangedOptionModule [ "services" "openldap" "logLevel" ] [ "services" "openldap" "settings" "attrs" "olcLogLevel" ]
92 (config: lib.splitString " " (lib.getAttrFromPath [ "services" "openldap" "logLevel" ] config)))
93 (lib.mkChangedOptionModule [ "services" "openldap" "defaultSchemas" ] [ "services" "openldap" "settings" "children" "cn=schema" "includes"]
94 (config: lib.optionals (lib.getAttrFromPath [ "services" "openldap" "defaultSchemas" ] config) (
95 map (schema: "${openldap}/etc/schema/${schema}.ldif") [ "core" "cosine" "inetorgperson" "nis" ])))
96
97 (lib.mkChangedOptionModule [ "services" "openldap" "database" ] [ "services" "openldap" "settings" "children" ]
98 (config: let
99 database = lib.getAttrFromPath [ "services" "openldap" "database" ] config;
100 in {
101 "olcDatabase={1}${database}".attrs = {
102 # objectClass is case-insensitive, so don't need to capitalize ${database}
103 objectClass = [ "olcdatabaseconfig" "olc${database}config" ];
104 olcDatabase = "{1}${database}";
105 olcDbDirectory = lib.mkDefault "/var/db/openldap";
106 };
107 "cn=schema".includes = lib.mkDefault (
108 map (schema: "${openldap}/etc/schema/${schema}.ldif") [ "core" "cosine" "inetorgperson" "nis" ]
109 );
110 }))
111 (mkDatabaseOption "rootpwFile" [ "olcRootPW" "path" ])
112 (mkDatabaseOption "suffix" [ "olcSuffix" ])
113 (mkDatabaseOption "dataDir" [ "olcDbDirectory" ])
114 (mkDatabaseOption "rootdn" [ "olcRootDN" ])
115 (mkDatabaseOption "rootpw" [ "olcRootPW" ])
116 ];
117 options = {
118 services.openldap = {
119 enable = mkOption {
120 type = types.bool;
121 default = false;
122 description = "
123 Whether to enable the ldap server.
124 ";
125 };
126
127 package = mkOption {
128 type = types.package;
129 default = pkgs.openldap;
130 description = ''
131 OpenLDAP package to use.
132
133 This can be used to, for example, set an OpenLDAP package
134 with custom overrides to enable modules or other
135 functionality.
136 '';
137 };
138
139 user = mkOption {
140 type = types.str;
141 default = "openldap";
142 description = "User account under which slapd runs.";
143 };
144
145 group = mkOption {
146 type = types.str;
147 default = "openldap";
148 description = "Group account under which slapd runs.";
149 };
150
151 urlList = mkOption {
152 type = types.listOf types.str;
153 default = [ "ldap:///" ];
154 description = "URL list slapd should listen on.";
155 example = [ "ldaps:///" ];
156 };
157
158 settings = mkOption {
159 type = ldapAttrsType;
160 description = "Configuration for OpenLDAP, in OLC format";
161 example = lib.literalExample ''
162 {
163 attrs.olcLogLevel = [ "stats" ];
164 children = {
165 "cn=schema".includes = [
166 "\${pkgs.openldap}/etc/schema/core.ldif"
167 "\${pkgs.openldap}/etc/schema/cosine.ldif"
168 "\${pkgs.openldap}/etc/schema/inetorgperson.ldif"
169 ];
170 "olcDatabase={-1}frontend" = {
171 attrs = {
172 objectClass = "olcDatabaseConfig";
173 olcDatabase = "{-1}frontend";
174 olcAccess = [ "{0}to * by dn.exact=uidNumber=0+gidNumber=0,cn=peercred,cn=external,cn=auth manage stop by * none stop" ];
175 };
176 };
177 "olcDatabase={0}config" = {
178 attrs = {
179 objectClass = "olcDatabaseConfig";
180 olcDatabase = "{0}config";
181 olcAccess = [ "{0}to * by * none break" ];
182 };
183 };
184 "olcDatabase={1}mdb" = {
185 attrs = {
186 objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ];
187 olcDatabase = "{1}mdb";
188 olcDbDirectory = "/var/db/ldap";
189 olcDbIndex = [
190 "objectClass eq"
191 "cn pres,eq"
192 "uid pres,eq"
193 "sn pres,eq,subany"
194 ];
195 olcSuffix = "dc=example,dc=com";
196 olcAccess = [ "{0}to * by * read break" ];
197 };
198 };
199 };
200 };
201 '';
202 };
203
204 # This option overrides settings
205 configDir = mkOption {
206 type = types.nullOr types.path;
207 default = null;
208 description = ''
209 Use this config directory instead of generating one from the
210 <literal>settings</literal> option. Overrides all NixOS settings. If
211 you use this option,ensure `olcPidFile` is set to `/run/slapd/slapd.conf`.
212 '';
213 example = "/var/db/slapd.d";
214 };
215
216 declarativeContents = mkOption {
217 type = with types; attrsOf lines;
218 default = {};
219 description = ''
220 Declarative contents for the LDAP database, in LDIF format by suffix.
221
222 All data will be erased when starting the LDAP server. Modifications
223 to the database are not prevented, they are just dropped on the next
224 reboot of the server. Performance-wise the database and indexes are
225 rebuilt on each server startup, so this will slow down server startup,
226 especially with large databases.
227 '';
228 example = lib.literalExample ''
229 {
230 "dc=example,dc=org" = '''
231 dn= dn: dc=example,dc=org
232 objectClass: domain
233 dc: example
234
235 dn: ou=users,dc=example,dc=org
236 objectClass = organizationalUnit
237 ou: users
238
239 # ...
240 ''';
241 }
242 '';
243 };
244 };
245 };
246
247 meta.maintainers = with lib.maintainers; [ mic92 kwohlfahrt ];
248
249 config = mkIf cfg.enable {
250 assertions = map (opt: {
251 assertion = ((getAttr opt cfg) != "_mkMergedOptionModule") -> (cfg.database != "_mkMergedOptionModule");
252 message = "Legacy OpenLDAP option `services.openldap.${opt}` requires `services.openldap.database` (use value \"mdb\" if unsure)";
253 }) legacyOptions;
254 environment.systemPackages = [ openldap ];
255
256 # Literal attributes must always be set
257 services.openldap.settings = {
258 attrs = {
259 objectClass = "olcGlobal";
260 cn = "config";
261 olcPidFile = "/run/slapd/slapd.pid";
262 };
263 children."cn=schema".attrs = {
264 cn = "schema";
265 objectClass = "olcSchemaConfig";
266 };
267 };
268
269 systemd.services.openldap = {
270 description = "LDAP server";
271 wantedBy = [ "multi-user.target" ];
272 after = [ "network.target" ];
273 preStart = let
274 settingsFile = pkgs.writeText "config.ldif" (lib.concatStringsSep "\n" (attrsToLdif "cn=config" cfg.settings));
275
276 dbSettings = lib.filterAttrs (name: value: lib.hasPrefix "olcDatabase=" name) cfg.settings.children;
277 dataDirs = lib.mapAttrs' (name: value: lib.nameValuePair value.attrs.olcSuffix value.attrs.olcDbDirectory)
278 (lib.filterAttrs (_: value: value.attrs ? olcDbDirectory) dbSettings);
279 dataFiles = lib.mapAttrs (dn: contents: pkgs.writeText "${dn}.ldif" contents) cfg.declarativeContents;
280 mkLoadScript = dn: let
281 dataDir = lib.escapeShellArg (getAttr dn dataDirs);
282 in ''
283 rm -rf ${dataDir}/*
284 ${openldap}/bin/slapadd -F ${lib.escapeShellArg configDir} -b ${dn} -l ${getAttr dn dataFiles}
285 chown -R "${cfg.user}:${cfg.group}" ${dataDir}
286 '';
287 in ''
288 mkdir -p /run/slapd
289 chown -R "${cfg.user}:${cfg.group}" /run/slapd
290
291 mkdir -p ${lib.escapeShellArg configDir} ${lib.escapeShellArgs (lib.attrValues dataDirs)}
292 chown "${cfg.user}:${cfg.group}" ${lib.escapeShellArg configDir} ${lib.escapeShellArgs (lib.attrValues dataDirs)}
293
294 ${lib.optionalString (cfg.configDir == null) (''
295 rm -Rf ${configDir}/*
296 ${openldap}/bin/slapadd -F ${configDir} -bcn=config -l ${settingsFile}
297 '')}
298 chown -R "${cfg.user}:${cfg.group}" ${lib.escapeShellArg configDir}
299
300 ${lib.concatStrings (map mkLoadScript (lib.attrNames cfg.declarativeContents))}
301 ${openldap}/bin/slaptest -u -F ${lib.escapeShellArg configDir}
302 '';
303 serviceConfig = {
304 ExecStart = lib.escapeShellArgs ([
305 "${openldap}/libexec/slapd" "-u" cfg.user "-g" cfg.group "-F" configDir
306 "-h" (lib.concatStringsSep " " cfg.urlList)
307 ]);
308 Type = "forking";
309 PIDFile = cfg.settings.attrs.olcPidFile;
310 };
311 };
312
313 users.users = lib.optionalAttrs (cfg.user == "openldap") {
314 openldap = {
315 group = cfg.group;
316 isSystemUser = true;
317 };
318 };
319
320 users.groups = lib.optionalAttrs (cfg.group == "openldap") {
321 openldap = {};
322 };
323 };
324}