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