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 assertion = (cfg.declarativeContents != { }) -> cfg.configDir == null;
284 message = ''
285 Declarative DB contents (${lib.attrNames cfg.declarativeContents}) are not
286 supported with user-managed configuration.
287 '';
288 }
289 ]
290 ++ (map (dn: {
291 assertion = (lib.getAttr dn dbSettings) ? "olcDbDirectory";
292 # olcDbDirectory is necessary to prepopulate database using `slapadd`.
293 message = ''
294 Declarative DB ${dn} does not exist in `services.openldap.settings`, or does not have
295 `olcDbDirectory` configured.
296 '';
297 }) (lib.attrNames cfg.declarativeContents))
298 ++ (lib.mapAttrsToList (
299 dn:
300 {
301 olcDbDirectory ? null,
302 ...
303 }:
304 {
305 # For forward compatibility with `DynamicUser`, and to avoid accidentally clobbering
306 # directories with `declarativeContents`.
307 assertion =
308 (olcDbDirectory != null)
309 -> (
310 (lib.hasPrefix "/var/lib/openldap/" olcDbDirectory) && (olcDbDirectory != "/var/lib/openldap/")
311 );
312 message = ''
313 Database ${dn} has `olcDbDirectory` (${olcDbDirectory}) that is not a subdirectory of
314 `/var/lib/openldap/`.
315 '';
316 }
317 ) dbSettings);
318 environment.systemPackages = [ openldap ];
319
320 # Literal attributes must always be set
321 services.openldap.settings = {
322 attrs = {
323 objectClass = "olcGlobal";
324 cn = "config";
325 };
326 children."cn=schema".attrs = {
327 cn = "schema";
328 objectClass = "olcSchemaConfig";
329 };
330 };
331
332 systemd.services.openldap = {
333 description = "OpenLDAP Server Daemon";
334 documentation = [
335 "man:slapd"
336 "man:slapd-config"
337 "man:slapd-mdb"
338 ];
339 wantedBy = [ "multi-user.target" ];
340 wants = [ "network-online.target" ];
341 after = [ "network-online.target" ];
342 serviceConfig = {
343 User = cfg.user;
344 Group = cfg.group;
345 ExecStartPre = [
346 "!${pkgs.coreutils}/bin/mkdir -p ${configDir}"
347 "+${pkgs.coreutils}/bin/chown $USER ${configDir}"
348 ]
349 ++ (lib.optional (cfg.configDir == null) writeConfig)
350 ++ (lib.mapAttrsToList (
351 dn: content:
352 lib.escapeShellArgs [
353 writeContents
354 dn
355 (lib.getAttr dn dbSettings).olcDbDirectory
356 content
357 ]
358 ) contentsFiles)
359 ++ [ "${openldap}/bin/slaptest -u -F ${configDir}" ];
360 ExecStart = lib.escapeShellArgs ([
361 "${openldap}/libexec/slapd"
362 "-d"
363 "0"
364 "-F"
365 configDir
366 "-h"
367 (lib.concatStringsSep " " cfg.urlList)
368 ]);
369 Type = "notify";
370 # Fixes an error where openldap attempts to notify from a thread
371 # outside the main process:
372 # Got notification message from PID 6378, but reception only permitted for main PID 6377
373 NotifyAccess = "all";
374 RuntimeDirectory = "openldap";
375 StateDirectory = [
376 "openldap"
377 ]
378 ++ (map ({ olcDbDirectory, ... }: lib.removePrefix "/var/lib/" olcDbDirectory) (
379 lib.attrValues dbSettings
380 ));
381 StateDirectoryMode = "700";
382 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
383 CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
384 };
385 };
386
387 users.users = lib.optionalAttrs (cfg.user == "openldap") {
388 openldap = {
389 group = cfg.group;
390 isSystemUser = true;
391 };
392 };
393
394 users.groups = lib.optionalAttrs (cfg.group == "openldap") {
395 openldap = { };
396 };
397 };
398}