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.literalExpression ''
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 defaultText = literalExpression "pkgs.openldap";
131 description = ''
132 OpenLDAP package to use.
133
134 This can be used to, for example, set an OpenLDAP package
135 with custom overrides to enable modules or other
136 functionality.
137 '';
138 };
139
140 user = mkOption {
141 type = types.str;
142 default = "openldap";
143 description = "User account under which slapd runs.";
144 };
145
146 group = mkOption {
147 type = types.str;
148 default = "openldap";
149 description = "Group account under which slapd runs.";
150 };
151
152 urlList = mkOption {
153 type = types.listOf types.str;
154 default = [ "ldap:///" ];
155 description = "URL list slapd should listen on.";
156 example = [ "ldaps:///" ];
157 };
158
159 settings = mkOption {
160 type = ldapAttrsType;
161 description = "Configuration for OpenLDAP, in OLC format";
162 example = lib.literalExpression ''
163 {
164 attrs.olcLogLevel = [ "stats" ];
165 children = {
166 "cn=schema".includes = [
167 "''${pkgs.openldap}/etc/schema/core.ldif"
168 "''${pkgs.openldap}/etc/schema/cosine.ldif"
169 "''${pkgs.openldap}/etc/schema/inetorgperson.ldif"
170 ];
171 "olcDatabase={-1}frontend" = {
172 attrs = {
173 objectClass = "olcDatabaseConfig";
174 olcDatabase = "{-1}frontend";
175 olcAccess = [ "{0}to * by dn.exact=uidNumber=0+gidNumber=0,cn=peercred,cn=external,cn=auth manage stop by * none stop" ];
176 };
177 };
178 "olcDatabase={0}config" = {
179 attrs = {
180 objectClass = "olcDatabaseConfig";
181 olcDatabase = "{0}config";
182 olcAccess = [ "{0}to * by * none break" ];
183 };
184 };
185 "olcDatabase={1}mdb" = {
186 attrs = {
187 objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ];
188 olcDatabase = "{1}mdb";
189 olcDbDirectory = "/var/db/ldap";
190 olcDbIndex = [
191 "objectClass eq"
192 "cn pres,eq"
193 "uid pres,eq"
194 "sn pres,eq,subany"
195 ];
196 olcSuffix = "dc=example,dc=com";
197 olcAccess = [ "{0}to * by * read break" ];
198 };
199 };
200 };
201 };
202 '';
203 };
204
205 # This option overrides settings
206 configDir = mkOption {
207 type = types.nullOr types.path;
208 default = null;
209 description = ''
210 Use this config directory instead of generating one from the
211 <literal>settings</literal> option. Overrides all NixOS settings. If
212 you use this option,ensure `olcPidFile` is set to `/run/slapd/slapd.conf`.
213 '';
214 example = "/var/db/slapd.d";
215 };
216
217 declarativeContents = mkOption {
218 type = with types; attrsOf lines;
219 default = {};
220 description = ''
221 Declarative contents for the LDAP database, in LDIF format by suffix.
222
223 All data will be erased when starting the LDAP server. Modifications
224 to the database are not prevented, they are just dropped on the next
225 reboot of the server. Performance-wise the database and indexes are
226 rebuilt on each server startup, so this will slow down server startup,
227 especially with large databases.
228 '';
229 example = lib.literalExpression ''
230 {
231 "dc=example,dc=org" = '''
232 dn= dn: dc=example,dc=org
233 objectClass: domain
234 dc: example
235
236 dn: ou=users,dc=example,dc=org
237 objectClass = organizationalUnit
238 ou: users
239
240 # ...
241 ''';
242 }
243 '';
244 };
245 };
246 };
247
248 meta.maintainers = with lib.maintainers; [ mic92 kwohlfahrt ];
249
250 config = mkIf cfg.enable {
251 assertions = map (opt: {
252 assertion = ((getAttr opt cfg) != "_mkMergedOptionModule") -> (cfg.database != "_mkMergedOptionModule");
253 message = "Legacy OpenLDAP option `services.openldap.${opt}` requires `services.openldap.database` (use value \"mdb\" if unsure)";
254 }) legacyOptions;
255 environment.systemPackages = [ openldap ];
256
257 # Literal attributes must always be set
258 services.openldap.settings = {
259 attrs = {
260 objectClass = "olcGlobal";
261 cn = "config";
262 olcPidFile = "/run/slapd/slapd.pid";
263 };
264 children."cn=schema".attrs = {
265 cn = "schema";
266 objectClass = "olcSchemaConfig";
267 };
268 };
269
270 systemd.services.openldap = {
271 description = "LDAP server";
272 wantedBy = [ "multi-user.target" ];
273 after = [ "network.target" ];
274 preStart = let
275 settingsFile = pkgs.writeText "config.ldif" (lib.concatStringsSep "\n" (attrsToLdif "cn=config" cfg.settings));
276
277 dbSettings = lib.filterAttrs (name: value: lib.hasPrefix "olcDatabase=" name) cfg.settings.children;
278 dataDirs = lib.mapAttrs' (name: value: lib.nameValuePair value.attrs.olcSuffix value.attrs.olcDbDirectory)
279 (lib.filterAttrs (_: value: value.attrs ? olcDbDirectory) dbSettings);
280 dataFiles = lib.mapAttrs (dn: contents: pkgs.writeText "${dn}.ldif" contents) cfg.declarativeContents;
281 mkLoadScript = dn: let
282 dataDir = lib.escapeShellArg (getAttr dn dataDirs);
283 in ''
284 rm -rf ${dataDir}/*
285 ${openldap}/bin/slapadd -F ${lib.escapeShellArg configDir} -b ${dn} -l ${getAttr dn dataFiles}
286 chown -R "${cfg.user}:${cfg.group}" ${dataDir}
287 '';
288 in ''
289 mkdir -p /run/slapd
290 chown -R "${cfg.user}:${cfg.group}" /run/slapd
291
292 mkdir -p ${lib.escapeShellArg configDir} ${lib.escapeShellArgs (lib.attrValues dataDirs)}
293 chown "${cfg.user}:${cfg.group}" ${lib.escapeShellArg configDir} ${lib.escapeShellArgs (lib.attrValues dataDirs)}
294
295 ${lib.optionalString (cfg.configDir == null) (''
296 rm -Rf ${configDir}/*
297 ${openldap}/bin/slapadd -F ${configDir} -bcn=config -l ${settingsFile}
298 '')}
299 chown -R "${cfg.user}:${cfg.group}" ${lib.escapeShellArg configDir}
300
301 ${lib.concatStrings (map mkLoadScript (lib.attrNames cfg.declarativeContents))}
302 ${openldap}/bin/slaptest -u -F ${lib.escapeShellArg configDir}
303 '';
304 serviceConfig = {
305 ExecStart = lib.escapeShellArgs ([
306 "${openldap}/libexec/slapd" "-u" cfg.user "-g" cfg.group "-F" configDir
307 "-h" (lib.concatStringsSep " " cfg.urlList)
308 ]);
309 Type = "forking";
310 PIDFile = cfg.settings.attrs.olcPidFile;
311 };
312 };
313
314 users.users = lib.optionalAttrs (cfg.user == "openldap") {
315 openldap = {
316 group = cfg.group;
317 isSystemUser = true;
318 };
319 };
320
321 users.groups = lib.optionalAttrs (cfg.group == "openldap") {
322 openldap = {};
323 };
324 };
325}