@ -1,20 +1,19 @@
{ config , lib , pkgs , . . . }:
with lib ;
let
cfg = config . services . openldap ;
openldap = cfg . package ;
dataFile = pkgs . writeText " l d a p - c o n t e n t s . l d i f " cfg . declarativeContents ;
configFile = pkgs . writeText " s l a p d . c o n f " ( ( optionalString cfg . defaultSchemas ''
include $ { openldap . out } /etc/schema/core.schema
include $ { openldap . out } /etc/schema/cosine.schema
include $ { openldap . out } /etc/schema/inetorgperson.schema
include $ { openldap . out } /etc/schema/nis.schema
configFile = pkgs . writeText " s l a p d . c o n f " ( ( optionalString ( cfg . defaultSchemas != null && cfg . defaultSchemas ) ''
include $ { openldap } /etc/schema/core.schema
include $ { openldap } /etc/schema/cosine.schema
include $ { openldap } /etc/schema/inetorgperson.schema
include $ { openldap } /etc/schema/nis.schema
'' ) + ''
$ { cfg . extraConfig }
pidfile /run/slapd/slapd.pid
$ { if cfg . extraConfig != null then cfg . extraConfig else " " }
database $ { cfg . database }
suffix $ { cfg . suffix }
rootdn $ { cfg . rootdn }
@ -24,20 +23,79 @@ let
include $ { cfg . rootpwFile }
'' }
directory $ { cfg . dataDir }
$ { cfg . extraDatabaseConfig }
$ { if cfg . extraDatabaseConfig != null then cfg . extraDatabaseConfig else " " }
'' ) ;
configOpts = if cfg . configDir == null then " - f ${ configFile } "
else " - F ${ cfg . configDir } " ;
in
{
###### interface
configDir = if cfg . configDir != null then cfg . configDir else " / e t c / o p e n l d a p / s l a p d . d " ;
ldapValueType = let
singleLdapValueType = types . either types . str ( types . submodule {
options = {
path = mkOption {
type = types . path ;
description = ''
A path containing the LDAP attribute . This is included at run-time , so
is recommended for storing secrets .
'' ;
} ;
} ;
} ) ;
in types . either singleLdapValueType ( types . listOf singleLdapValueType ) ;
ldapAttrsType =
let
options = {
attrs = mkOption {
type = types . attrsOf ldapValueType ;
default = { } ;
description = " A t t r i b u t e s o f t h e p a r e n t e n t r y . " ;
} ;
children = mkOption {
# Hide the child attributes, to avoid infinite recursion in e.g. documentation
# Actual Nix evaluation is lazy, so this is not an issue there
type = let
hiddenOptions = lib . mapAttrs ( name : attr : attr // { visible = false ; } ) options ;
in types . attrsOf ( types . submodule { options = hiddenOptions ; } ) ;
default = { } ;
description = " C h i l d e n t r i e s o f t h e c u r r e n t e n t r y , w i t h r e c u r s i v e l y t h e s a m e s t r u c t u r e . " ;
example = lib . literalExample ''
{
" c n = s c h e m a " = {
# The attribute used in the DN must be defined
attrs = { cn = " s c h e m a " ; } ;
children = {
# This entry's DN is expanded to "cn=foo,cn=schema"
" c n = f o o " = { . . . } ;
} ;
# These includes are inserted after "cn=schema", but before "cn=foo,cn=schema"
includes = [ . . . ] ;
} ;
}
'' ;
} ;
includes = mkOption {
type = types . listOf types . path ;
default = [ ] ;
description = ''
LDIF files to include after the parent's attributes but before its children .
'' ;
} ;
} ;
in types . submodule { inherit options ; } ;
valueToLdif = attr : values : let
singleValueToLdif = value : if lib . isAttrs value then " ${ attr } : < f i l e : / / ${ value . path } " else " ${ attr } : ${ value } " ;
in if lib . isList values then map singleValueToLdif values else [ ( singleValueToLdif values ) ] ;
attrsToLdif = dn : { attrs , children , includes , . . . }: [ ''
dn : $ { dn }
$ { lib . concatStringsSep " \n " ( lib . flatten ( lib . mapAttrsToList valueToLdif attrs ) ) }
'' ] + + ( m a p ( p a t h : " i n c l u d e : f i l e : / / ${ path } \ n " ) i n c l u d e s ) + + (
lib . flatten ( lib . mapAttrsToList ( name : value : attrsToLdif " ${ name } , ${ dn } " value ) children )
) ;
in {
options = {
services . openldap = {
enable = mkOption {
type = types . bool ;
default = false ;
@ -77,47 +135,91 @@ in
example = [ " l d a p s : / / / " ] ;
} ;
settings = mkOption {
type = ldapAttrsType ;
description = " C o n f i g u r a t i o n f o r O p e n L D A P , i n O L C f o r m a t " ;
example = lib . literalExample ''
{
attrs . olcLogLevel = [ " s t a t s " ] ;
children = {
" c n = s c h e m a " . includes = [
" \$ { p k g s . o p e n l d a p } / e t c / s c h e m a / c o r e . l d i f "
" \$ { p k g s . o p e n l d a p } / e t c / s c h e m a / c o s i n e . l d i f "
" \$ { p k g s . o p e n l d a p } / e t c / s c h e m a / i n e t o r g p e r s o n . l d i f "
] ;
" o l c D a t a b a s e = { - 1 } f r o n t e n d " = {
attrs = {
objectClass = " o l c D a t a b a s e C o n f i g " ;
olcDatabase = " { - 1 } f r o n t e n d " ;
olcAccess = [ " { 0 } t o * b y d n . e x a c t = u i d N u m b e r = 0 + g i d N u m b e r = 0 , c n = p e e r c r e d , c n = e x t e r n a l , c n = a u t h m a n a g e s t o p b y * n o n e s t o p " ] ;
} ;
} ;
" o l c D a t a b a s e = { 0 } c o n f i g " = {
attrs = {
objectClass = " o l c D a t a b a s e C o n f i g " ;
olcDatabase = " { 0 } c o n f i g " ;
olcAccess = [ " { 0 } t o * b y * n o n e b r e a k " ] ;
} ;
} ;
" o l c D a t a b a s e = { 1 } m d b " = {
attrs = {
objectClass = [ " o l c D a t a b a s e C o n f i g " " o l c M d b C o n f i g " ] ;
olcDatabase = " { 1 } m d b " ;
olcDbDirectory = " / v a r / d b / l d a p " ;
olcDbIndex = [
" o b j e c t C l a s s e q "
" c n p r e s , e q "
" u i d p r e s , e q "
" s n p r e s , e q , s u b a n y "
] ;
olcSuffix = " d c = e x a m p l e , d c = c o m " ;
olcAccess = [ " { 0 } t o * b y * r e a d b r e a k " ] ;
} ;
} ;
} ;
} ;
'' ;
} ;
# These options are translated into settings
dataDir = mkOption {
type = types . path ;
type = types . nullOr types . path ;
default = " / v a r / d b / o p e n l d a p " ;
description = " T h e d a t a b a s e d i r e c t o r y . " ;
} ;
defaultSchemas = mkOption {
type = types . bool ;
type = types . nullOr types . bool ;
default = true ;
description = ''
Include the default schemas core , cosine , inetorgperson and nis .
This setting will be ignored if configDir is set .
'' ;
} ;
database = mkOption {
type = types . str ;
type = types . nullOr types . str ;
default = " m d b " ;
description = ''
Database type to use for the LDAP .
This setting will be ignored if configDir is set .
'' ;
description = " B a c k e n d t o u s e f o r t h e f i r s t d a t a b a s e . " ;
} ;
suffix = mkOption {
type = types . str ;
type = types . nullOr types . str ;
default = null ;
example = " d c = e x a m p l e , d c = o r g " ;
description = ''
Specify the DN suffix of queries that will be passed to this backend
database .
This setting will be ignored if configDir is set .
Specify the DN suffix of queries that will be passed to the first
database database .
'' ;
} ;
rootdn = mkOption {
type = types . str ;
type = types . nullOr types . str ;
default = null ;
example = " c n = a d m i n , d c = e x a m p l e , d c = o r g " ;
description = ''
Specify the distinguished name that is not subject to access control
or administrative limit restrictions for operations on this database .
This setting will be ignored if configDir is set .
'' ;
} ;
@ -125,10 +227,9 @@ in
type = types . nullOr types . str ;
default = null ;
description = ''
Password for the root user .
This setting will be ignored if configDir is set .
Using this option will store the root password in plain text in the
world-readable nix store . To avoid this the <literal> rootpwFile < /literal > can be used .
Password for the root user . Using this option will store the root
password in plain text in the world-readable nix store . To avoid this
the <literal> rootpwFile < /literal > can be used .
'' ;
} ;
@ -137,25 +238,36 @@ in
default = null ;
description = ''
Password file for the root user .
The file should contain the string <literal> rootpw < /literal > followed by the password .
e . g . : <literal> rootpw mysecurepassword < /literal >
If the deprecated <literal> extraConfig < /literal > or
<literal> extraDatabaseConfig < /literal > options are set , this should
contain <literal> rootpw < /literal > followed by the password
( e . g . <literal> rootpw thePasswordHere < /literal > ) .
Otherwise the file should contain only the password ( no trailing
newline or leading <literal> rootpw < /literal > ) .
'' ;
} ;
logLevel = mkOption {
type = types . str ;
default = " 0 " ;
example = "a c l t r a c e " ;
description = " T h e l o g l e v e l s e l e c t o r o f s l a p d ." ;
type = types . nullOr ( types . listOf types . str ) ;
default = null ;
example = literalExample "[ \" ac l \" \" t r a c e \" ] " ;
description = " T h e l o g l e v e l . " ;
} ;
# This option overrides settings
configDir = mkOption {
type = types . nullOr types . path ;
default = null ;
description = " U s e t h i s o p t i o n a l c o n f i g d i r e c t o r y i n s t e a d o f u s i n g s l a p d . c o n f " ;
description = ''
Use this optional config directory instead of generating one from the
<literal> settings < /literal > option .
'' ;
example = " / v a r / d b / s l a p d . d " ;
} ;
# These options are deprecated
extraConfig = mkOption {
type = types . lines ;
default = " " ;
@ -164,10 +276,10 @@ in
" ;
example = literalExample ''
'' '
include $ { openldap . out } /etc/schema/core.schema
include $ { openldap . out } /etc/schema/cosine.schema
include $ { openldap . out } /etc/schema/inetorgperson.schema
include $ { openldap . out } /etc/schema/nis.schema
include $ { openldap } /etc/schema/core.schema
include $ { openldap } /etc/schema/cosine.schema
include $ { openldap } /etc/schema/inetorgperson.schema
include $ { openldap } /etc/schema/nis.schema
database bdb
suffix dc = example , dc = org
@ -244,57 +356,156 @@ in
} ;
meta = {
maintainers = [ lib . maintainers . mic92 ] ;
maintainers = with lib . maintainters ; [ mic92 kwohlfahrt ] ;
} ;
###### implementation
config = mkIf cfg . enable {
assertions = [
{
assertion = cfg . configDir != null || cfg . rootpwFile != null || cfg . rootpw != null ;
message = " s e r v i c e s . o p e n l d a p : U n l e s s c o n f i g D i r i s s e t , e i t h e r r o o t p w o r r o o t p w F i l e m u s t b e s e t " ;
}
] ;
warnings = let
deprecations = [
{ old = " l o g L e v e l " ; new = " a t t r s . o l c L o g L e v e l " ; }
{ old = " d e f a u l t S c h e m a s " ;
new = " c h i l d r e n . \" c n = s c h e m a \" . i n c l u d e s " ;
newValue = " [ \n ${ lib . concatStringsSep " \n " [
" \$ { p k g s . o p e n l d a p } / e t c / s c h e m a / c o r e . l d i f "
" \$ { p k g s . o p e n l d a p } / e t c / s c h e m a / c o s i n e . l d i f "
" \$ { p k g s . o p e n l d a p } / e t c / s c h e m a / i n e t o r g p e r s o n . l d i f "
" \$ { p k g s . o p e n l d a p } / e t c / s c h e m a / n i s . l d i f "
] } \ n ] " ; }
{ old = " d a t a b a s e " ; new = " c h i l d r e n . \" c n = { 1 } ${ cfg . database } \" " ; newValue = " { } " ; }
{ old = " s u f f i x " ; new = " c h i l d r e n . \" c n = { 1 } ${ cfg . database } \" . a t t r s . o l c S u f f i x " ; }
{ old = " d a t a D i r " ; new = " c h i l d r e n . \" c n = { 1 } ${ cfg . database } \" . a t t r s . o l c D b D i r e c t o r y " ; }
{ old = " r o o t d n " ; new = " c h i l d r e n . \" c n = { 1 } ${ cfg . database } \" . a t t r s . o l c R o o t D N " ; }
{ old = " r o o t p w " ; new = " c h i l d r e n . \" c n = { 1 } ${ cfg . database } \" . a t t r s . o l c R o o t P W " ; }
{ old = " r o o t p w F i l e " ;
new = " c h i l d r e n . \" c n = { 1 } ${ cfg . database } \" . a t t r s . o l c R o o t P W " ;
newValue = " { p a t h = \" ${ cfg . rootpwFile } \" ; } " ;
note = " T h e f i l e s h o u l d c o n t a i n o n l y t h e p a s s w o r d ( w i t h o u t \" r o o t p w \" a s b e f o r e ) " ; }
] ;
in ( optional ( cfg . extraConfig != " " || cfg . extraDatabaseConfig != " " ) ''
The options ` extraConfig ` and ` extraDatabaseConfig ` of ` services . openldap `
are deprecated . This is due to the deprecation of ` slapd . conf `
upstream . Please migrate to ` services . openldap . settings ` .
After deploying this configuration , you can run :
slapcat - F $ { configDir } - n0 - H ' ldap:///??? ( ! ( objectClass = olcSchemaConfig ) ) '
on the same host to print your current configuration in LDIF format ,
which should be straightforward to convert into Nix settings .
'' ) + + ( f l a t t e n ( m a p ( a r g s @ { o l d , n e w , . . . } : l i b . o p t i o n a l ( ( l i b . h a s A t t r o l d c f g ) & & ( l i b . g e t A t t r o l d c f g ) ! = n u l l ) ''
The attribute ` services . openldap . ${ old } ` is deprecated . Please set it to
` null ` and use the following option instead :
services . openldap . settings . ${ new } = $ { args . newValue or (
let oldValue = ( getAttr old cfg ) ;
in if ( isList oldValue ) then " [ ${ concatStringsSep " " oldValue } ] " else oldValue
) }
'' ) d e p r e c a t i o n s ) ) + + ( o p t i o n a l ( c f g . c o n f i g D i r ! = n u l l & & ( v e r s i o n O l d e r c o n f i g . s y s t e m . s t a t e V e r s i o n " 2 0 . 0 9 " ) ) ''
The attribute ` services . openldap . settings ` now exists , and may be more
useful than ` services . openldap . configDir ` . If you continue to use
` configDir ` , ensure that ` olcPidFile ` is set to " / r u n / s l a p d / s l a p d . p i d " .
Set ` system . stateVersion ` to " 2 0 . 0 9 " or greater to silence this message .
'' ) ;
assertions = [ {
assertion = ! ( cfg . rootpwFile != null && cfg . rootpw != null ) ;
message = " s e r v i c e s . o p e n l d a p : a t m o s t o n e o f r o o t p w o r r o o t p w F i l e m u s t b e s e t " ;
} ] ;
environment . systemPackages = [ openldap ] ;
# Literal attributes must always be set (even if other top-level attributres are deprecated)
services . openldap . settings = {
attrs = {
objectClass = " o l c G l o b a l " ;
cn = " c o n f i g " ;
olcPidFile = " / r u n / s l a p d / s l a p d . p i d " ;
} // ( lib . optionalAttrs ( cfg . logLevel != null ) {
olcLogLevel = cfg . logLevel ;
} ) ;
children = {
" c n = s c h e m a " = {
attrs = {
cn = " s c h e m a " ;
objectClass = " o l c S c h e m a C o n f i g " ;
} ;
includes = lib . optionals ( cfg . defaultSchemas != null && cfg . defaultSchemas ) [
" ${ openldap } / e t c / s c h e m a / c o r e . l d i f "
" ${ openldap } / e t c / s c h e m a / c o s i n e . l d i f "
" ${ openldap } / e t c / s c h e m a / i n e t o r g p e r s o n . l d i f "
" ${ openldap } / e t c / s c h e m a / n i s . l d i f "
] ;
} ;
} // ( lib . optionalAttrs ( cfg . database != null ) {
" o l c D a t a b a s e = { 1 } ${ cfg . database } " . attrs = {
# objectClass is case-insensitive, so don't need to capitalize ${database}
objectClass = [ " o l c d a t a b a s e c o n f i g " " o l c ${ cfg . database } c o n f i g " ] ;
olcDatabase = " { 1 } ${ cfg . database } " ;
} // ( lib . optionalAttrs ( cfg . suffix != null ) {
olcSuffix = cfg . suffix ;
} ) // ( lib . optionalAttrs ( cfg . dataDir != null ) {
olcDbDirectory = cfg . dataDir ;
} ) // ( lib . optionalAttrs ( cfg . rootdn != null ) {
olcRootDN = cfg . rootdn ; # TODO: Optional
} ) // ( lib . optionalAttrs ( cfg . rootpw != null || cfg . rootpwFile != null ) {
olcRootPW = ( if cfg . rootpwFile != null then { path = cfg . rootpwFile ; } else cfg . rootpw ) ; # TODO: Optional
} ) ;
} ) ;
} ;
systemd . services . openldap = {
description = " L D A P s e r v e r " ;
wantedBy = [ " m u l t i - u s e r . t a r g e t " ] ;
after = [ " n e t w o r k . t a r g e t " ] ;
preStart = ''
preStart = let
dbSettings = lib . filterAttrs ( name : value : lib . hasPrefix " o l c D a t a b a s e = " name ) cfg . settings . children ;
dataDirs = lib . mapAttrsToList ( name : value : value . attrs . olcDbDirectory ) dbSettings ;
settingsFile = pkgs . writeText " c o n f i g . l d i f " ( lib . concatStringsSep " \n " ( attrsToLdif " c n = c o n f i g " cfg . settings ) ) ;
in ''
mkdir - p /run/slapd
chown - R " ${ cfg . user } : ${ cfg . group } " /run/slapd
mkdir - p ' $ { configDir } ' $ { lib . escapeShellArgs dataDirs }
chown " ${ cfg . user } : ${ cfg . group } " ' $ { configDir } ' $ { lib . escapeShellArgs dataDirs }
$ { lib . optionalString ( cfg . configDir == null ) (
if ( cfg . extraConfig != " " || cfg . extraDatabaseConfig != " " ) then ''
rm - Rf ' $ { configDir } ' /*
# -u disables config generation, so just ignore the return code
$ { openldap } /bin/slaptest - f $ { configFile } - F $ { configDir } || true
'' e l s e ''
rm - Rf ' $ { configDir } ' /*
$ { openldap } /bin/slapadd - F $ { configDir } - n0 - l $ { settingsFile }
''
) }
chown - R " ${ cfg . user } : ${ cfg . group } " ' $ { configDir } '
$ { optionalString ( cfg . declarativeContents != null ) ''
rm - Rf " ${ cfg . dataDir } "
'' }
mkdir - p " ${ cfg . dataDir } "
$ { optionalString ( cfg . declarativeContents != null ) ''
$ { openldap . out } /bin/slapadd $ { configOpts } - l $ { dataFile }
rm - Rf ' $ { lib . head dataDirs } ' /*
$ { openldap } /bin/slapadd - F $ { configDir } - n1 - l $ { dataFile }
chown - R " ${ cfg . user } : ${ cfg . group } " $ { lib . escapeShellArgs dataDirs }
'' }
chown - R " ${ cfg . user } : ${ cfg . group } " " ${ cfg . dataDir } "
$ { openldap } /bin/slaptest $ { configOpts }
$ { openldap } /bin/slaptest - u - F $ { configDir }
'' ;
serviceConfig . ExecStart =
" ${ openldap . out } / l i b e x e c / s l a p d - d ' ${ cfg . logLevel } ' " +
" - u ' ${ cfg . user } ' - g ' ${ cfg . group } ' " +
" - h ' ${ concatStringsSep " " cfg . urlList } ' " +
" ${ configOpts } " ;
} ;
users . users . openldap =
{ name = cfg . user ;
group = cfg . group ;
uid = config . ids . uids . openldap ;
serviceConfig = {
ExecStart = lib . concatStringsSep " " [
" ${ openldap } / l i b e x e c / s l a p d "
" - u ' ${ cfg . user } ' "
" - g ' ${ cfg . group } ' "
" - h ' ${ concatStringsSep " " cfg . urlList } ' "
" - F ${ configDir } "
] ;
Type = " f o r k i n g " ;
PIDFile = cfg . settings . attrs . olcPidFile ;
} ;
} ;
users . groups . openldap =
{ name = cfg . group ;
gid = config . ids . gids . openldap ;
} ;
users . users = lib . optionalAttrs ( cfg . user == " o p e n l d a p " ) {
openldap = { group = cfg . group ; } ;
} ;
users . groups = lib . optionalAttrs ( cfg . group == " o p e n l d a p " ) {
openldap = { } ;
} ;
} ;
}