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