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