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 = "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 = "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 = ''
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 = "Whether to enable the ldap server.";
92 };
93
94 package = mkPackageOption pkgs "openldap" {
95 extraDescription = ''
96 This can be used to, for example, set an OpenLDAP package
97 with custom overrides to enable modules or other
98 functionality.
99 '';
100 };
101
102 user = mkOption {
103 type = types.str;
104 default = "openldap";
105 description = "User account under which slapd runs.";
106 };
107
108 group = mkOption {
109 type = types.str;
110 default = "openldap";
111 description = "Group account under which slapd runs.";
112 };
113
114 urlList = mkOption {
115 type = types.listOf types.str;
116 default = [ "ldap:///" ];
117 description = "URL list slapd should listen on.";
118 example = [ "ldaps:///" ];
119 };
120
121 settings = mkOption {
122 type = ldapAttrsType;
123 description = "Configuration for OpenLDAP, in OLC format";
124 example = lib.literalExpression ''
125 {
126 attrs.olcLogLevel = [ "stats" ];
127 children = {
128 "cn=schema".includes = [
129 "''${pkgs.openldap}/etc/schema/core.ldif"
130 "''${pkgs.openldap}/etc/schema/cosine.ldif"
131 "''${pkgs.openldap}/etc/schema/inetorgperson.ldif"
132 ];
133 "olcDatabase={-1}frontend" = {
134 attrs = {
135 objectClass = "olcDatabaseConfig";
136 olcDatabase = "{-1}frontend";
137 olcAccess = [ "{0}to * by dn.exact=uidNumber=0+gidNumber=0,cn=peercred,cn=external,cn=auth manage stop by * none stop" ];
138 };
139 };
140 "olcDatabase={0}config" = {
141 attrs = {
142 objectClass = "olcDatabaseConfig";
143 olcDatabase = "{0}config";
144 olcAccess = [ "{0}to * by * none break" ];
145 };
146 };
147 "olcDatabase={1}mdb" = {
148 attrs = {
149 objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ];
150 olcDatabase = "{1}mdb";
151 olcDbDirectory = "/var/lib/openldap/ldap";
152 olcDbIndex = [
153 "objectClass eq"
154 "cn pres,eq"
155 "uid pres,eq"
156 "sn pres,eq,subany"
157 ];
158 olcSuffix = "dc=example,dc=com";
159 olcAccess = [ "{0}to * by * read break" ];
160 };
161 };
162 };
163 };
164 '';
165 };
166
167 # This option overrides settings
168 configDir = mkOption {
169 type = types.nullOr types.path;
170 default = null;
171 description = ''
172 Use this config directory instead of generating one from the
173 `settings` option. Overrides all NixOS settings.
174 '';
175 example = "/var/lib/openldap/slapd.d";
176 };
177
178 mutableConfig = mkOption {
179 type = types.bool;
180 default = false;
181 description = ''
182 Whether to allow writable on-line configuration. If
183 `true`, the NixOS settings will only be used to
184 initialize the OpenLDAP configuration if it does not exist, and are
185 subsequently ignored.
186 '';
187 };
188
189 declarativeContents = mkOption {
190 type = with types; attrsOf lines;
191 default = {};
192 description = ''
193 Declarative contents for the LDAP database, in LDIF format by suffix.
194
195 All data will be erased when starting the LDAP server. Modifications
196 to the database are not prevented, they are just dropped on the next
197 reboot of the server. Performance-wise the database and indexes are
198 rebuilt on each server startup, so this will slow down server startup,
199 especially with large databases.
200
201 Note that the root of the DB must be defined in
202 `services.openldap.settings` and the
203 `olcDbDirectory` must begin with
204 `"/var/lib/openldap"`.
205 '';
206 example = lib.literalExpression ''
207 {
208 "dc=example,dc=org" = '''
209 dn= dn: dc=example,dc=org
210 objectClass: domain
211 dc: example
212
213 dn: ou=users,dc=example,dc=org
214 objectClass = organizationalUnit
215 ou: users
216
217 # ...
218 ''';
219 }
220 '';
221 };
222 };
223 };
224
225 meta.maintainers = with lib.maintainers; [ kwohlfahrt ];
226
227 config = let
228 dbSettings = mapAttrs' (name: { attrs, ... }: nameValuePair attrs.olcSuffix attrs)
229 (filterAttrs (name: { attrs, ... }: (hasPrefix "olcDatabase=" name) && attrs ? olcSuffix) cfg.settings.children);
230 settingsFile = pkgs.writeText "config.ldif" (lib.concatStringsSep "\n" (attrsToLdif "cn=config" cfg.settings));
231 writeConfig = pkgs.writeShellScript "openldap-config" ''
232 set -euo pipefail
233
234 ${lib.optionalString (!cfg.mutableConfig) ''
235 chmod -R u+w ${configDir}
236 rm -rf ${configDir}/*
237 ''}
238 if [ ! -e "${configDir}/cn=config.ldif" ]; then
239 ${openldap}/bin/slapadd -F ${configDir} -bcn=config -l ${settingsFile}
240 fi
241 chmod -R ${if cfg.mutableConfig then "u+rw" else "u+r-w"} ${configDir}
242 '';
243
244 contentsFiles = mapAttrs (dn: ldif: pkgs.writeText "${dn}.ldif" ldif) cfg.declarativeContents;
245 writeContents = pkgs.writeShellScript "openldap-load" ''
246 set -euo pipefail
247
248 rm -rf $2/*
249 ${openldap}/bin/slapadd -F ${configDir} -b $1 -l $3
250 '';
251 in mkIf cfg.enable {
252 assertions = [{
253 assertion = (cfg.declarativeContents != {}) -> cfg.configDir == null;
254 message = ''
255 Declarative DB contents (${attrNames cfg.declarativeContents}) are not
256 supported with user-managed configuration.
257 '';
258 }] ++ (map (dn: {
259 assertion = (getAttr dn dbSettings) ? "olcDbDirectory";
260 # olcDbDirectory is necessary to prepopulate database using `slapadd`.
261 message = ''
262 Declarative DB ${dn} does not exist in `services.openldap.settings`, or does not have
263 `olcDbDirectory` configured.
264 '';
265 }) (attrNames cfg.declarativeContents)) ++ (mapAttrsToList (dn: { olcDbDirectory ? null, ... }: {
266 # For forward compatibility with `DynamicUser`, and to avoid accidentally clobbering
267 # directories with `declarativeContents`.
268 assertion = (olcDbDirectory != null) ->
269 ((hasPrefix "/var/lib/openldap/" olcDbDirectory) && (olcDbDirectory != "/var/lib/openldap/"));
270 message = ''
271 Database ${dn} has `olcDbDirectory` (${olcDbDirectory}) that is not a subdirectory of
272 `/var/lib/openldap/`.
273 '';
274 }) dbSettings);
275 environment.systemPackages = [ openldap ];
276
277 # Literal attributes must always be set
278 services.openldap.settings = {
279 attrs = {
280 objectClass = "olcGlobal";
281 cn = "config";
282 };
283 children."cn=schema".attrs = {
284 cn = "schema";
285 objectClass = "olcSchemaConfig";
286 };
287 };
288
289 systemd.services.openldap = {
290 description = "OpenLDAP Server Daemon";
291 documentation = [
292 "man:slapd"
293 "man:slapd-config"
294 "man:slapd-mdb"
295 ];
296 wantedBy = [ "multi-user.target" ];
297 wants = [ "network-online.target" ];
298 after = [ "network-online.target" ];
299 serviceConfig = {
300 User = cfg.user;
301 Group = cfg.group;
302 ExecStartPre = [
303 "!${pkgs.coreutils}/bin/mkdir -p ${configDir}"
304 "+${pkgs.coreutils}/bin/chown $USER ${configDir}"
305 ] ++ (lib.optional (cfg.configDir == null) writeConfig)
306 ++ (mapAttrsToList (dn: content: lib.escapeShellArgs [
307 writeContents dn (getAttr dn dbSettings).olcDbDirectory content
308 ]) contentsFiles)
309 ++ [ "${openldap}/bin/slaptest -u -F ${configDir}" ];
310 ExecStart = lib.escapeShellArgs ([
311 "${openldap}/libexec/slapd" "-d" "0" "-F" configDir "-h" (lib.concatStringsSep " " cfg.urlList)
312 ]);
313 Type = "notify";
314 # Fixes an error where openldap attempts to notify from a thread
315 # outside the main process:
316 # Got notification message from PID 6378, but reception only permitted for main PID 6377
317 NotifyAccess = "all";
318 RuntimeDirectory = "openldap";
319 StateDirectory = ["openldap"]
320 ++ (map ({olcDbDirectory, ... }: removePrefix "/var/lib/" olcDbDirectory) (attrValues dbSettings));
321 StateDirectoryMode = "700";
322 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
323 CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
324 };
325 };
326
327 users.users = lib.optionalAttrs (cfg.user == "openldap") {
328 openldap = {
329 group = cfg.group;
330 isSystemUser = true;
331 };
332 };
333
334 users.groups = lib.optionalAttrs (cfg.group == "openldap") {
335 openldap = {};
336 };
337 };
338}