at master 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 assertion = (cfg.declarativeContents != { }) -> cfg.configDir == null; 284 message = '' 285 Declarative DB contents (${lib.attrNames cfg.declarativeContents}) are not 286 supported with user-managed configuration. 287 ''; 288 } 289 ] 290 ++ (map (dn: { 291 assertion = (lib.getAttr dn dbSettings) ? "olcDbDirectory"; 292 # olcDbDirectory is necessary to prepopulate database using `slapadd`. 293 message = '' 294 Declarative DB ${dn} does not exist in `services.openldap.settings`, or does not have 295 `olcDbDirectory` configured. 296 ''; 297 }) (lib.attrNames cfg.declarativeContents)) 298 ++ (lib.mapAttrsToList ( 299 dn: 300 { 301 olcDbDirectory ? null, 302 ... 303 }: 304 { 305 # For forward compatibility with `DynamicUser`, and to avoid accidentally clobbering 306 # directories with `declarativeContents`. 307 assertion = 308 (olcDbDirectory != null) 309 -> ( 310 (lib.hasPrefix "/var/lib/openldap/" olcDbDirectory) && (olcDbDirectory != "/var/lib/openldap/") 311 ); 312 message = '' 313 Database ${dn} has `olcDbDirectory` (${olcDbDirectory}) that is not a subdirectory of 314 `/var/lib/openldap/`. 315 ''; 316 } 317 ) dbSettings); 318 environment.systemPackages = [ openldap ]; 319 320 # Literal attributes must always be set 321 services.openldap.settings = { 322 attrs = { 323 objectClass = "olcGlobal"; 324 cn = "config"; 325 }; 326 children."cn=schema".attrs = { 327 cn = "schema"; 328 objectClass = "olcSchemaConfig"; 329 }; 330 }; 331 332 systemd.services.openldap = { 333 description = "OpenLDAP Server Daemon"; 334 documentation = [ 335 "man:slapd" 336 "man:slapd-config" 337 "man:slapd-mdb" 338 ]; 339 wantedBy = [ "multi-user.target" ]; 340 wants = [ "network-online.target" ]; 341 after = [ "network-online.target" ]; 342 serviceConfig = { 343 User = cfg.user; 344 Group = cfg.group; 345 ExecStartPre = [ 346 "!${pkgs.coreutils}/bin/mkdir -p ${configDir}" 347 "+${pkgs.coreutils}/bin/chown $USER ${configDir}" 348 ] 349 ++ (lib.optional (cfg.configDir == null) writeConfig) 350 ++ (lib.mapAttrsToList ( 351 dn: content: 352 lib.escapeShellArgs [ 353 writeContents 354 dn 355 (lib.getAttr dn dbSettings).olcDbDirectory 356 content 357 ] 358 ) contentsFiles) 359 ++ [ "${openldap}/bin/slaptest -u -F ${configDir}" ]; 360 ExecStart = lib.escapeShellArgs ([ 361 "${openldap}/libexec/slapd" 362 "-d" 363 "0" 364 "-F" 365 configDir 366 "-h" 367 (lib.concatStringsSep " " cfg.urlList) 368 ]); 369 Type = "notify"; 370 # Fixes an error where openldap attempts to notify from a thread 371 # outside the main process: 372 # Got notification message from PID 6378, but reception only permitted for main PID 6377 373 NotifyAccess = "all"; 374 RuntimeDirectory = "openldap"; 375 StateDirectory = [ 376 "openldap" 377 ] 378 ++ (map ({ olcDbDirectory, ... }: lib.removePrefix "/var/lib/" olcDbDirectory) ( 379 lib.attrValues dbSettings 380 )); 381 StateDirectoryMode = "700"; 382 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; 383 CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; 384 }; 385 }; 386 387 users.users = lib.optionalAttrs (cfg.user == "openldap") { 388 openldap = { 389 group = cfg.group; 390 isSystemUser = true; 391 }; 392 }; 393 394 users.groups = lib.optionalAttrs (cfg.group == "openldap") { 395 openldap = { }; 396 }; 397 }; 398}