at 23.11-pre 13 kB view raw
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}