1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.services.openldap;
9 openldap = cfg.package;
10 configDir = if cfg.configDir != null then cfg.configDir else "/etc/openldap/slapd.d";
11
12 ldapValueType =
13 let
14 # Can't do types.either with multiple non-overlapping submodules, so define our own
15 singleLdapValueType = lib.mkOptionType rec {
16 name = "LDAP";
17 # TODO: It would be nice to define a { secret = ...; } option, using
18 # systemd's LoadCredentials for secrets. That would remove the last
19 # barrier to using DynamicUser for openldap. This is blocked on
20 # systemd/systemd#19604
21 description = ''
22 LDAP value - either a string, or an attrset containing
23 `path` or `base64` for included
24 values or base-64 encoded values respectively.
25 '';
26 check = x: lib.isString x || (lib.isAttrs x && (x ? path || x ? base64));
27 merge = lib.mergeEqualOption;
28 };
29 in
30 # We don't coerce to lists of single values, as some values must be unique
31 lib.types.either singleLdapValueType (lib.types.listOf singleLdapValueType);
32
33 ldapAttrsType =
34 let
35 options = {
36 attrs = lib.mkOption {
37 type = lib.types.attrsOf ldapValueType;
38 default = { };
39 description = "Attributes of the parent entry.";
40 };
41 children = lib.mkOption {
42 # Hide the child attributes, to avoid infinite recursion in e.g. documentation
43 # Actual Nix evaluation is lazy, so this is not an issue there
44 type =
45 let
46 hiddenOptions = lib.mapAttrs (name: attr: attr // { visible = false; }) options;
47 in
48 lib.types.attrsOf (lib.types.submodule { options = hiddenOptions; });
49 default = { };
50 description = "Child entries of the current entry, with recursively the same structure.";
51 example = lib.literalExpression ''
52 {
53 "cn=schema" = {
54 # The attribute used in the DN must be defined
55 attrs = { cn = "schema"; };
56 children = {
57 # This entry's DN is expanded to "cn=foo,cn=schema"
58 "cn=foo" = { ... };
59 };
60 # These includes are inserted after "cn=schema", but before "cn=foo,cn=schema"
61 includes = [ ... ];
62 };
63 }
64 '';
65 };
66 includes = lib.mkOption {
67 type = lib.types.listOf lib.types.path;
68 default = [ ];
69 description = ''
70 LDIF files to include after the parent's attributes but before its children.
71 '';
72 };
73 };
74 in
75 lib.types.submodule { inherit options; };
76
77 valueToLdif =
78 attr: values:
79 let
80 listValues = if lib.isList values then values else lib.singleton values;
81 in
82 map (
83 value:
84 if lib.isAttrs value then
85 if lib.hasAttr "path" value then "${attr}:< file://${value.path}" else "${attr}:: ${value.base64}"
86 else
87 "${attr}: ${lib.replaceStrings [ "\n" ] [ "\n " ] value}"
88 ) listValues;
89
90 attrsToLdif =
91 dn:
92 {
93 attrs,
94 children,
95 includes,
96 ...
97 }:
98 [
99 ''
100 dn: ${dn}
101 ${lib.concatStringsSep "\n" (lib.flatten (lib.mapAttrsToList valueToLdif attrs))}
102 ''
103 ]
104 ++ (map (path: "include: file://${path}\n") includes)
105 ++ (lib.flatten (lib.mapAttrsToList (name: value: attrsToLdif "${name},${dn}" value) children));
106in
107{
108 options = {
109 services.openldap = {
110 enable = lib.mkOption {
111 type = lib.types.bool;
112 default = false;
113 description = "Whether to enable the ldap server.";
114 };
115
116 package = lib.mkPackageOption pkgs "openldap" {
117 extraDescription = ''
118 This can be used to, for example, set an OpenLDAP package
119 with custom overrides to enable modules or other
120 functionality.
121 '';
122 };
123
124 user = lib.mkOption {
125 type = lib.types.str;
126 default = "openldap";
127 description = "User account under which slapd runs.";
128 };
129
130 group = lib.mkOption {
131 type = lib.types.str;
132 default = "openldap";
133 description = "Group account under which slapd runs.";
134 };
135
136 urlList = lib.mkOption {
137 type = lib.types.listOf lib.types.str;
138 default = [ "ldap:///" ];
139 description = "URL list slapd should listen on.";
140 example = [ "ldaps:///" ];
141 };
142
143 settings = lib.mkOption {
144 type = ldapAttrsType;
145 description = "Configuration for OpenLDAP, in OLC format";
146 example = lib.literalExpression ''
147 {
148 attrs.olcLogLevel = [ "stats" ];
149 children = {
150 "cn=schema".includes = [
151 "''${pkgs.openldap}/etc/schema/core.ldif"
152 "''${pkgs.openldap}/etc/schema/cosine.ldif"
153 "''${pkgs.openldap}/etc/schema/inetorgperson.ldif"
154 ];
155 "olcDatabase={-1}frontend" = {
156 attrs = {
157 objectClass = "olcDatabaseConfig";
158 olcDatabase = "{-1}frontend";
159 olcAccess = [ "{0}to * by dn.exact=uidNumber=0+gidNumber=0,cn=peercred,cn=external,cn=auth manage stop by * none stop" ];
160 };
161 };
162 "olcDatabase={0}config" = {
163 attrs = {
164 objectClass = "olcDatabaseConfig";
165 olcDatabase = "{0}config";
166 olcAccess = [ "{0}to * by * none break" ];
167 };
168 };
169 "olcDatabase={1}mdb" = {
170 attrs = {
171 objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ];
172 olcDatabase = "{1}mdb";
173 olcDbDirectory = "/var/lib/openldap/ldap";
174 olcDbIndex = [
175 "objectClass eq"
176 "cn pres,eq"
177 "uid pres,eq"
178 "sn pres,eq,subany"
179 ];
180 olcSuffix = "dc=example,dc=com";
181 olcAccess = [ "{0}to * by * read break" ];
182 };
183 };
184 };
185 };
186 '';
187 };
188
189 # This option overrides settings
190 configDir = lib.mkOption {
191 type = lib.types.nullOr lib.types.path;
192 default = null;
193 description = ''
194 Use this config directory instead of generating one from the
195 `settings` option. Overrides all NixOS settings.
196 '';
197 example = "/var/lib/openldap/slapd.d";
198 };
199
200 mutableConfig = lib.mkOption {
201 type = lib.types.bool;
202 default = false;
203 description = ''
204 Whether to allow writable on-line configuration. If
205 `true`, the NixOS settings will only be used to
206 initialize the OpenLDAP configuration if it does not exist, and are
207 subsequently ignored.
208 '';
209 };
210
211 declarativeContents = lib.mkOption {
212 type = with lib.types; attrsOf lines;
213 default = { };
214 description = ''
215 Declarative contents for the LDAP database, in LDIF format by suffix.
216
217 All data will be erased when starting the LDAP server. Modifications
218 to the database are not prevented, they are just dropped on the next
219 reboot of the server. Performance-wise the database and indexes are
220 rebuilt on each server startup, so this will slow down server startup,
221 especially with large databases.
222
223 Note that the root of the DB must be defined in
224 `services.openldap.settings` and the
225 `olcDbDirectory` must begin with
226 `"/var/lib/openldap"`.
227 '';
228 example = lib.literalExpression ''
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; [ kwohlfahrt ];
248
249 config =
250 let
251 dbSettings = lib.mapAttrs' (name: { attrs, ... }: lib.nameValuePair attrs.olcSuffix attrs) (
252 lib.filterAttrs (
253 name: { attrs, ... }: (lib.hasPrefix "olcDatabase=" name) && attrs ? olcSuffix
254 ) cfg.settings.children
255 );
256 settingsFile = pkgs.writeText "config.ldif" (
257 lib.concatStringsSep "\n" (attrsToLdif "cn=config" cfg.settings)
258 );
259 writeConfig = pkgs.writeShellScript "openldap-config" ''
260 set -euo pipefail
261
262 ${lib.optionalString (!cfg.mutableConfig) ''
263 chmod -R u+w ${configDir}
264 rm -rf ${configDir}/*
265 ''}
266 if [ ! -e "${configDir}/cn=config.ldif" ]; then
267 ${openldap}/bin/slapadd -F ${configDir} -bcn=config -l ${settingsFile}
268 fi
269 chmod -R ${if cfg.mutableConfig then "u+rw" else "u+r-w"} ${configDir}
270 '';
271
272 contentsFiles = lib.mapAttrs (dn: ldif: pkgs.writeText "${dn}.ldif" ldif) cfg.declarativeContents;
273 writeContents = pkgs.writeShellScript "openldap-load" ''
274 set -euo pipefail
275
276 rm -rf $2/*
277 ${openldap}/bin/slapadd -F ${configDir} -b $1 -l $3
278 '';
279 in
280 lib.mkIf cfg.enable {
281 assertions =
282 [
283 {
284 assertion = (cfg.declarativeContents != { }) -> cfg.configDir == null;
285 message = ''
286 Declarative DB contents (${lib.attrNames cfg.declarativeContents}) are not
287 supported with user-managed configuration.
288 '';
289 }
290 ]
291 ++ (map (dn: {
292 assertion = (lib.getAttr dn dbSettings) ? "olcDbDirectory";
293 # olcDbDirectory is necessary to prepopulate database using `slapadd`.
294 message = ''
295 Declarative DB ${dn} does not exist in `services.openldap.settings`, or does not have
296 `olcDbDirectory` configured.
297 '';
298 }) (lib.attrNames cfg.declarativeContents))
299 ++ (lib.mapAttrsToList (
300 dn:
301 {
302 olcDbDirectory ? null,
303 ...
304 }:
305 {
306 # For forward compatibility with `DynamicUser`, and to avoid accidentally clobbering
307 # directories with `declarativeContents`.
308 assertion =
309 (olcDbDirectory != null)
310 -> (
311 (lib.hasPrefix "/var/lib/openldap/" olcDbDirectory) && (olcDbDirectory != "/var/lib/openldap/")
312 );
313 message = ''
314 Database ${dn} has `olcDbDirectory` (${olcDbDirectory}) that is not a subdirectory of
315 `/var/lib/openldap/`.
316 '';
317 }
318 ) dbSettings);
319 environment.systemPackages = [ openldap ];
320
321 # Literal attributes must always be set
322 services.openldap.settings = {
323 attrs = {
324 objectClass = "olcGlobal";
325 cn = "config";
326 };
327 children."cn=schema".attrs = {
328 cn = "schema";
329 objectClass = "olcSchemaConfig";
330 };
331 };
332
333 systemd.services.openldap = {
334 description = "OpenLDAP Server Daemon";
335 documentation = [
336 "man:slapd"
337 "man:slapd-config"
338 "man:slapd-mdb"
339 ];
340 wantedBy = [ "multi-user.target" ];
341 wants = [ "network-online.target" ];
342 after = [ "network-online.target" ];
343 serviceConfig = {
344 User = cfg.user;
345 Group = cfg.group;
346 ExecStartPre =
347 [
348 "!${pkgs.coreutils}/bin/mkdir -p ${configDir}"
349 "+${pkgs.coreutils}/bin/chown $USER ${configDir}"
350 ]
351 ++ (lib.optional (cfg.configDir == null) writeConfig)
352 ++ (lib.mapAttrsToList (
353 dn: content:
354 lib.escapeShellArgs [
355 writeContents
356 dn
357 (lib.getAttr dn dbSettings).olcDbDirectory
358 content
359 ]
360 ) contentsFiles)
361 ++ [ "${openldap}/bin/slaptest -u -F ${configDir}" ];
362 ExecStart = lib.escapeShellArgs ([
363 "${openldap}/libexec/slapd"
364 "-d"
365 "0"
366 "-F"
367 configDir
368 "-h"
369 (lib.concatStringsSep " " cfg.urlList)
370 ]);
371 Type = "notify";
372 # Fixes an error where openldap attempts to notify from a thread
373 # outside the main process:
374 # Got notification message from PID 6378, but reception only permitted for main PID 6377
375 NotifyAccess = "all";
376 RuntimeDirectory = "openldap";
377 StateDirectory =
378 [ "openldap" ]
379 ++ (map ({ olcDbDirectory, ... }: lib.removePrefix "/var/lib/" olcDbDirectory) (
380 lib.attrValues dbSettings
381 ));
382 StateDirectoryMode = "700";
383 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
384 CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
385 };
386 };
387
388 users.users = lib.optionalAttrs (cfg.user == "openldap") {
389 openldap = {
390 group = cfg.group;
391 isSystemUser = true;
392 };
393 };
394
395 users.groups = lib.optionalAttrs (cfg.group == "openldap") {
396 openldap = { };
397 };
398 };
399}