at 24.11-pre 12 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 = "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 = "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 = '' 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 = "Whether to enable the ldap server."; 92 }; 93 94 package = mkPackageOption pkgs "openldap" { 95 extraDescription = '' 96 This can be used to, for example, set an OpenLDAP package 97 with custom overrides to enable modules or other 98 functionality. 99 ''; 100 }; 101 102 user = mkOption { 103 type = types.str; 104 default = "openldap"; 105 description = "User account under which slapd runs."; 106 }; 107 108 group = mkOption { 109 type = types.str; 110 default = "openldap"; 111 description = "Group account under which slapd runs."; 112 }; 113 114 urlList = mkOption { 115 type = types.listOf types.str; 116 default = [ "ldap:///" ]; 117 description = "URL list slapd should listen on."; 118 example = [ "ldaps:///" ]; 119 }; 120 121 settings = mkOption { 122 type = ldapAttrsType; 123 description = "Configuration for OpenLDAP, in OLC format"; 124 example = lib.literalExpression '' 125 { 126 attrs.olcLogLevel = [ "stats" ]; 127 children = { 128 "cn=schema".includes = [ 129 "''${pkgs.openldap}/etc/schema/core.ldif" 130 "''${pkgs.openldap}/etc/schema/cosine.ldif" 131 "''${pkgs.openldap}/etc/schema/inetorgperson.ldif" 132 ]; 133 "olcDatabase={-1}frontend" = { 134 attrs = { 135 objectClass = "olcDatabaseConfig"; 136 olcDatabase = "{-1}frontend"; 137 olcAccess = [ "{0}to * by dn.exact=uidNumber=0+gidNumber=0,cn=peercred,cn=external,cn=auth manage stop by * none stop" ]; 138 }; 139 }; 140 "olcDatabase={0}config" = { 141 attrs = { 142 objectClass = "olcDatabaseConfig"; 143 olcDatabase = "{0}config"; 144 olcAccess = [ "{0}to * by * none break" ]; 145 }; 146 }; 147 "olcDatabase={1}mdb" = { 148 attrs = { 149 objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ]; 150 olcDatabase = "{1}mdb"; 151 olcDbDirectory = "/var/lib/openldap/ldap"; 152 olcDbIndex = [ 153 "objectClass eq" 154 "cn pres,eq" 155 "uid pres,eq" 156 "sn pres,eq,subany" 157 ]; 158 olcSuffix = "dc=example,dc=com"; 159 olcAccess = [ "{0}to * by * read break" ]; 160 }; 161 }; 162 }; 163 }; 164 ''; 165 }; 166 167 # This option overrides settings 168 configDir = mkOption { 169 type = types.nullOr types.path; 170 default = null; 171 description = '' 172 Use this config directory instead of generating one from the 173 `settings` option. Overrides all NixOS settings. 174 ''; 175 example = "/var/lib/openldap/slapd.d"; 176 }; 177 178 mutableConfig = mkOption { 179 type = types.bool; 180 default = false; 181 description = '' 182 Whether to allow writable on-line configuration. If 183 `true`, the NixOS settings will only be used to 184 initialize the OpenLDAP configuration if it does not exist, and are 185 subsequently ignored. 186 ''; 187 }; 188 189 declarativeContents = mkOption { 190 type = with types; attrsOf lines; 191 default = {}; 192 description = '' 193 Declarative contents for the LDAP database, in LDIF format by suffix. 194 195 All data will be erased when starting the LDAP server. Modifications 196 to the database are not prevented, they are just dropped on the next 197 reboot of the server. Performance-wise the database and indexes are 198 rebuilt on each server startup, so this will slow down server startup, 199 especially with large databases. 200 201 Note that the root of the DB must be defined in 202 `services.openldap.settings` and the 203 `olcDbDirectory` must begin with 204 `"/var/lib/openldap"`. 205 ''; 206 example = lib.literalExpression '' 207 { 208 "dc=example,dc=org" = ''' 209 dn= dn: dc=example,dc=org 210 objectClass: domain 211 dc: example 212 213 dn: ou=users,dc=example,dc=org 214 objectClass = organizationalUnit 215 ou: users 216 217 # ... 218 '''; 219 } 220 ''; 221 }; 222 }; 223 }; 224 225 meta.maintainers = with lib.maintainers; [ kwohlfahrt ]; 226 227 config = let 228 dbSettings = mapAttrs' (name: { attrs, ... }: nameValuePair attrs.olcSuffix attrs) 229 (filterAttrs (name: { attrs, ... }: (hasPrefix "olcDatabase=" name) && attrs ? olcSuffix) cfg.settings.children); 230 settingsFile = pkgs.writeText "config.ldif" (lib.concatStringsSep "\n" (attrsToLdif "cn=config" cfg.settings)); 231 writeConfig = pkgs.writeShellScript "openldap-config" '' 232 set -euo pipefail 233 234 ${lib.optionalString (!cfg.mutableConfig) '' 235 chmod -R u+w ${configDir} 236 rm -rf ${configDir}/* 237 ''} 238 if [ ! -e "${configDir}/cn=config.ldif" ]; then 239 ${openldap}/bin/slapadd -F ${configDir} -bcn=config -l ${settingsFile} 240 fi 241 chmod -R ${if cfg.mutableConfig then "u+rw" else "u+r-w"} ${configDir} 242 ''; 243 244 contentsFiles = mapAttrs (dn: ldif: pkgs.writeText "${dn}.ldif" ldif) cfg.declarativeContents; 245 writeContents = pkgs.writeShellScript "openldap-load" '' 246 set -euo pipefail 247 248 rm -rf $2/* 249 ${openldap}/bin/slapadd -F ${configDir} -b $1 -l $3 250 ''; 251 in mkIf cfg.enable { 252 assertions = [{ 253 assertion = (cfg.declarativeContents != {}) -> cfg.configDir == null; 254 message = '' 255 Declarative DB contents (${attrNames cfg.declarativeContents}) are not 256 supported with user-managed configuration. 257 ''; 258 }] ++ (map (dn: { 259 assertion = (getAttr dn dbSettings) ? "olcDbDirectory"; 260 # olcDbDirectory is necessary to prepopulate database using `slapadd`. 261 message = '' 262 Declarative DB ${dn} does not exist in `services.openldap.settings`, or does not have 263 `olcDbDirectory` configured. 264 ''; 265 }) (attrNames cfg.declarativeContents)) ++ (mapAttrsToList (dn: { olcDbDirectory ? null, ... }: { 266 # For forward compatibility with `DynamicUser`, and to avoid accidentally clobbering 267 # directories with `declarativeContents`. 268 assertion = (olcDbDirectory != null) -> 269 ((hasPrefix "/var/lib/openldap/" olcDbDirectory) && (olcDbDirectory != "/var/lib/openldap/")); 270 message = '' 271 Database ${dn} has `olcDbDirectory` (${olcDbDirectory}) that is not a subdirectory of 272 `/var/lib/openldap/`. 273 ''; 274 }) dbSettings); 275 environment.systemPackages = [ openldap ]; 276 277 # Literal attributes must always be set 278 services.openldap.settings = { 279 attrs = { 280 objectClass = "olcGlobal"; 281 cn = "config"; 282 }; 283 children."cn=schema".attrs = { 284 cn = "schema"; 285 objectClass = "olcSchemaConfig"; 286 }; 287 }; 288 289 systemd.services.openldap = { 290 description = "OpenLDAP Server Daemon"; 291 documentation = [ 292 "man:slapd" 293 "man:slapd-config" 294 "man:slapd-mdb" 295 ]; 296 wantedBy = [ "multi-user.target" ]; 297 wants = [ "network-online.target" ]; 298 after = [ "network-online.target" ]; 299 serviceConfig = { 300 User = cfg.user; 301 Group = cfg.group; 302 ExecStartPre = [ 303 "!${pkgs.coreutils}/bin/mkdir -p ${configDir}" 304 "+${pkgs.coreutils}/bin/chown $USER ${configDir}" 305 ] ++ (lib.optional (cfg.configDir == null) writeConfig) 306 ++ (mapAttrsToList (dn: content: lib.escapeShellArgs [ 307 writeContents dn (getAttr dn dbSettings).olcDbDirectory content 308 ]) contentsFiles) 309 ++ [ "${openldap}/bin/slaptest -u -F ${configDir}" ]; 310 ExecStart = lib.escapeShellArgs ([ 311 "${openldap}/libexec/slapd" "-d" "0" "-F" configDir "-h" (lib.concatStringsSep " " cfg.urlList) 312 ]); 313 Type = "notify"; 314 # Fixes an error where openldap attempts to notify from a thread 315 # outside the main process: 316 # Got notification message from PID 6378, but reception only permitted for main PID 6377 317 NotifyAccess = "all"; 318 RuntimeDirectory = "openldap"; 319 StateDirectory = ["openldap"] 320 ++ (map ({olcDbDirectory, ... }: removePrefix "/var/lib/" olcDbDirectory) (attrValues dbSettings)); 321 StateDirectoryMode = "700"; 322 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; 323 CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; 324 }; 325 }; 326 327 users.users = lib.optionalAttrs (cfg.user == "openldap") { 328 openldap = { 329 group = cfg.group; 330 isSystemUser = true; 331 }; 332 }; 333 334 users.groups = lib.optionalAttrs (cfg.group == "openldap") { 335 openldap = {}; 336 }; 337 }; 338}