406 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
			
		
		
	
	
			406 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
| import ./make-test-python.nix ({ pkgs, lib, ...} :
 | |
| 
 | |
| let
 | |
|   unlines = lib.concatStringsSep "\n";
 | |
|   unlinesAttrs = f: as: unlines (lib.mapAttrsToList f as);
 | |
| 
 | |
|   dbDomain = "example.com";
 | |
|   dbSuffix = "dc=example,dc=com";
 | |
|   dbAdminDn = "cn=admin,${dbSuffix}";
 | |
|   dbAdminPwd = "admin-password";
 | |
|   # NOTE: slappasswd -h "{SSHA}" -s '${dbAdminPwd}'
 | |
|   dbAdminPwdHash = "{SSHA}i7FopSzkFQMrHzDMB1vrtkI0rBnwouP8";
 | |
|   ldapUser = "test-ldap-user";
 | |
|   ldapUserId = 10000;
 | |
|   ldapUserPwd = "user-password";
 | |
|   # NOTE: slappasswd -h "{SSHA}" -s '${ldapUserPwd}'
 | |
|   ldapUserPwdHash = "{SSHA}v12XICMZNGT6r2KJ26rIkN8Vvvp4QX6i";
 | |
|   ldapGroup = "test-ldap-group";
 | |
|   ldapGroupId = 10000;
 | |
| 
 | |
|   mkClient = useDaemon:
 | |
|     { lib, ... }:
 | |
|     {
 | |
|       virtualisation.memorySize = 256;
 | |
|       virtualisation.vlans = [ 1 ];
 | |
|       security.pam.services.su.rootOK = lib.mkForce false;
 | |
|       users.ldap.enable = true;
 | |
|       users.ldap.daemon = {
 | |
|         enable = useDaemon;
 | |
|         rootpwmoddn = "cn=admin,${dbSuffix}";
 | |
|         rootpwmodpwFile = "/etc/nslcd.rootpwmodpw";
 | |
|       };
 | |
|       users.ldap.loginPam = true;
 | |
|       users.ldap.nsswitch = true;
 | |
|       users.ldap.server = "ldap://server";
 | |
|       users.ldap.base = "ou=posix,${dbSuffix}";
 | |
|       users.ldap.bind = {
 | |
|         distinguishedName = "cn=admin,${dbSuffix}";
 | |
|         passwordFile = "/etc/ldap/bind.password";
 | |
|       };
 | |
|       # NOTE: passwords stored in clear in Nix's store, but this is a test.
 | |
|       environment.etc."ldap/bind.password".source = pkgs.writeText "password" dbAdminPwd;
 | |
|       environment.etc."nslcd.rootpwmodpw".source = pkgs.writeText "rootpwmodpw" dbAdminPwd;
 | |
|     };
 | |
| in
 | |
| 
 | |
| {
 | |
|   name = "ldap";
 | |
|   meta = with pkgs.stdenv.lib.maintainers; {
 | |
|     maintainers = [ montag451 ];
 | |
|   };
 | |
| 
 | |
|   nodes = {
 | |
| 
 | |
|     server =
 | |
|       { pkgs, config, ... }:
 | |
|       let
 | |
|         inherit (config.services) openldap;
 | |
| 
 | |
|         slapdConfig = pkgs.writeText "cn=config.ldif" (''
 | |
|           dn: cn=config
 | |
|           objectClass: olcGlobal
 | |
|           #olcPidFile: /run/slapd/slapd.pid
 | |
|           # List of arguments that were passed to the server
 | |
|           #olcArgsFile: /run/slapd/slapd.args
 | |
|           # Read slapd-config(5) for possible values
 | |
|           olcLogLevel: none
 | |
|           # The tool-threads parameter sets the actual amount of CPU's
 | |
|           # that is used for indexing.
 | |
|           olcToolThreads: 1
 | |
| 
 | |
|           dn: olcDatabase={-1}frontend,cn=config
 | |
|           objectClass: olcDatabaseConfig
 | |
|           objectClass: olcFrontendConfig
 | |
|           # The maximum number of entries that is returned for a search operation
 | |
|           olcSizeLimit: 500
 | |
|           # Allow unlimited access to local connection from the local root user
 | |
|           olcAccess: to *
 | |
|             by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage
 | |
|             by * break
 | |
|           # Allow unauthenticated read access for schema and base DN autodiscovery
 | |
|           olcAccess: to dn.exact=""
 | |
|             by * read
 | |
|           olcAccess: to dn.base="cn=Subschema"
 | |
|             by * read
 | |
| 
 | |
|           dn: olcDatabase=config,cn=config
 | |
|           objectClass: olcDatabaseConfig
 | |
|           olcRootDN: cn=admin,cn=config
 | |
|           #olcRootPW:
 | |
|           # NOTE: access to cn=config, system root can be manager
 | |
|           # with SASL mechanism (-Y EXTERNAL) over unix socket (-H ldapi://)
 | |
|           olcAccess: to *
 | |
|             by dn.exact="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" manage
 | |
|             by * break
 | |
| 
 | |
|           dn: cn=schema,cn=config
 | |
|           objectClass: olcSchemaConfig
 | |
| 
 | |
|           include: file://${pkgs.openldap}/etc/schema/core.ldif
 | |
|           include: file://${pkgs.openldap}/etc/schema/cosine.ldif
 | |
|           include: file://${pkgs.openldap}/etc/schema/nis.ldif
 | |
|           include: file://${pkgs.openldap}/etc/schema/inetorgperson.ldif
 | |
| 
 | |
|           dn: cn=module{0},cn=config
 | |
|           objectClass: olcModuleList
 | |
|           # Where the dynamically loaded modules are stored
 | |
|           #olcModulePath: /usr/lib/ldap
 | |
|           olcModuleLoad: back_mdb
 | |
| 
 | |
|           ''
 | |
|           + unlinesAttrs (olcSuffix: {conf, ...}:
 | |
|               "include: file://" + pkgs.writeText "config.ldif" conf
 | |
|             ) slapdDatabases
 | |
|           );
 | |
| 
 | |
|         slapdDatabases = {
 | |
|           ${dbSuffix} = {
 | |
|             conf = ''
 | |
|               dn: olcBackend={1}mdb,cn=config
 | |
|               objectClass: olcBackendConfig
 | |
| 
 | |
|               dn: olcDatabase={1}mdb,cn=config
 | |
|               olcSuffix: ${dbSuffix}
 | |
|               olcDbDirectory: ${openldap.dataDir}/${dbSuffix}
 | |
|               objectClass: olcDatabaseConfig
 | |
|               objectClass: olcMdbConfig
 | |
|               # NOTE: checkpoint the database periodically in case of system failure
 | |
|               # and to speed up slapd shutdown.
 | |
|               olcDbCheckpoint: 512 30
 | |
|               # Database max size is 1G
 | |
|               olcDbMaxSize: 1073741824
 | |
|               olcLastMod: TRUE
 | |
|               # NOTE: database superuser. Needed for syncrepl,
 | |
|               # and used to auth as admin through a TCP connection.
 | |
|               olcRootDN: cn=admin,${dbSuffix}
 | |
|               olcRootPW: ${dbAdminPwdHash}
 | |
|               #
 | |
|               olcDbIndex: objectClass eq
 | |
|               olcDbIndex: cn,uid eq
 | |
|               olcDbIndex: uidNumber,gidNumber eq
 | |
|               olcDbIndex: member,memberUid eq
 | |
|               #
 | |
|               olcAccess: to attrs=userPassword
 | |
|                 by self write
 | |
|                 by anonymous auth
 | |
|                 by dn="cn=admin,${dbSuffix}" write
 | |
|                 by dn="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" write
 | |
|                 by * none
 | |
|               olcAccess: to attrs=shadowLastChange
 | |
|                 by self write
 | |
|                 by dn="cn=admin,${dbSuffix}" write
 | |
|                 by dn="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" write
 | |
|                 by * none
 | |
|               olcAccess: to dn.sub="ou=posix,${dbSuffix}"
 | |
|                 by self read
 | |
|                 by dn="cn=admin,${dbSuffix}" read
 | |
|                 by dn="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" read
 | |
|               olcAccess: to *
 | |
|                 by self read
 | |
|                 by * none
 | |
|             '';
 | |
|             data = ''
 | |
|               dn: ${dbSuffix}
 | |
|               objectClass: top
 | |
|               objectClass: dcObject
 | |
|               objectClass: organization
 | |
|               o: ${dbDomain}
 | |
| 
 | |
|               dn: cn=admin,${dbSuffix}
 | |
|               objectClass: simpleSecurityObject
 | |
|               objectClass: organizationalRole
 | |
|               description: ${dbDomain} LDAP administrator
 | |
|               roleOccupant: ${dbSuffix}
 | |
|               userPassword: ${ldapUserPwdHash}
 | |
| 
 | |
|               dn: ou=posix,${dbSuffix}
 | |
|               objectClass: top
 | |
|               objectClass: organizationalUnit
 | |
| 
 | |
|               dn: ou=accounts,ou=posix,${dbSuffix}
 | |
|               objectClass: top
 | |
|               objectClass: organizationalUnit
 | |
| 
 | |
|               dn: ou=groups,ou=posix,${dbSuffix}
 | |
|               objectClass: top
 | |
|               objectClass: organizationalUnit
 | |
|             ''
 | |
|             + lib.concatMapStrings posixAccount [
 | |
|               { uid=ldapUser; uidNumber=ldapUserId; gidNumber=ldapGroupId; userPassword=ldapUserPwdHash; }
 | |
|             ]
 | |
|             + lib.concatMapStrings posixGroup [
 | |
|               { gid=ldapGroup; gidNumber=ldapGroupId; members=[]; }
 | |
|             ];
 | |
|           };
 | |
|         };
 | |
| 
 | |
|         # NOTE: create a user account using the posixAccount objectClass.
 | |
|         posixAccount =
 | |
|           { uid
 | |
|           , uidNumber ? null
 | |
|           , gidNumber ? null
 | |
|           , cn ? ""
 | |
|           , sn ? ""
 | |
|           , userPassword ? ""
 | |
|           , loginShell ? "/bin/sh"
 | |
|           }: ''
 | |
| 
 | |
|             dn: uid=${uid},ou=accounts,ou=posix,${dbSuffix}
 | |
|             objectClass: person
 | |
|             objectClass: posixAccount
 | |
|             objectClass: shadowAccount
 | |
|             cn: ${cn}
 | |
|             gecos:
 | |
|             ${if gidNumber == null then "#" else "gidNumber: ${toString gidNumber}"}
 | |
|             homeDirectory: /home/${uid}
 | |
|             loginShell: ${loginShell}
 | |
|             sn: ${sn}
 | |
|             ${if uidNumber == null then "#" else "uidNumber: ${toString uidNumber}"}
 | |
|             ${if userPassword == "" then "#" else "userPassword: ${userPassword}"}
 | |
|           '';
 | |
| 
 | |
|         # NOTE: create a group using the posixGroup objectClass.
 | |
|         posixGroup =
 | |
|           { gid
 | |
|           , gidNumber
 | |
|           , members
 | |
|           }: ''
 | |
| 
 | |
|             dn: cn=${gid},ou=groups,ou=posix,${dbSuffix}
 | |
|             objectClass: top
 | |
|             objectClass: posixGroup
 | |
|             gidNumber: ${toString gidNumber}
 | |
|             ${lib.concatMapStrings (member: "memberUid: ${member}\n") members}
 | |
|           '';
 | |
|       in
 | |
|       {
 | |
|         virtualisation.memorySize = 256;
 | |
|         virtualisation.vlans = [ 1 ];
 | |
|         networking.firewall.allowedTCPPorts = [ 389 ];
 | |
|         services.openldap.enable = true;
 | |
|         services.openldap.dataDir = "/var/db/openldap";
 | |
|         services.openldap.configDir = "/var/db/slapd";
 | |
|         services.openldap.urlList = [
 | |
|           "ldap:///"
 | |
|           "ldapi:///"
 | |
|         ];
 | |
|         systemd.services.openldap = {
 | |
|           preStart = ''
 | |
|               set -e
 | |
|               # NOTE: slapd's config is always re-initialized.
 | |
|               rm -rf "${openldap.configDir}"/cn=config \
 | |
|                      "${openldap.configDir}"/cn=config.ldif
 | |
|               install -D -d -m 0700 -o "${openldap.user}" -g "${openldap.group}" "${openldap.configDir}"
 | |
|               # NOTE: olcDbDirectory must be created before adding the config.
 | |
|               '' +
 | |
|               unlinesAttrs (olcSuffix: {data, ...}: ''
 | |
|                 # NOTE: database is always re-initialized.
 | |
|                 rm -rf "${openldap.dataDir}/${olcSuffix}"
 | |
|                 install -D -d -m 0700 -o "${openldap.user}" -g "${openldap.group}" \
 | |
|                  "${openldap.dataDir}/${olcSuffix}"
 | |
|                 '') slapdDatabases
 | |
|               + ''
 | |
|               # NOTE: slapd is supposed to be stopped while in preStart,
 | |
|               #       hence slap* commands can safely be used.
 | |
|               umask 0077
 | |
|               ${pkgs.openldap}/bin/slapadd -n 0 \
 | |
|                -F "${openldap.configDir}" \
 | |
|                -l ${slapdConfig}
 | |
|               chown -R "${openldap.user}:${openldap.group}" "${openldap.configDir}"
 | |
|               # NOTE: slapadd(8): To populate the config database slapd-config(5),
 | |
|               #                   use -n 0 as it is always the first database.
 | |
|               #                   It must physically exist on the filesystem prior to this, however.
 | |
|             '' +
 | |
|             unlinesAttrs (olcSuffix: {data, ...}: ''
 | |
|               # NOTE: load database ${olcSuffix}
 | |
|               # (as root to avoid depending on sudo or chpst)
 | |
|               ${pkgs.openldap}/bin/slapadd \
 | |
|                -F "${openldap.configDir}" \
 | |
|                -l ${pkgs.writeText "data.ldif" data}
 | |
|               '' + ''
 | |
|               # NOTE: redundant with default openldap's preStart, but do not harm.
 | |
|               chown -R "${openldap.user}:${openldap.group}" \
 | |
|                "${openldap.dataDir}/${olcSuffix}"
 | |
|             '') slapdDatabases;
 | |
|         };
 | |
|       };
 | |
| 
 | |
|     client1 = mkClient true; # use nss_pam_ldapd
 | |
|     client2 = mkClient false; # use nss_ldap and pam_ldap
 | |
|   };
 | |
| 
 | |
|   testScript = ''
 | |
|     def expect_script(*commands):
 | |
|         script = ";".join(commands)
 | |
|         return f"${pkgs.expect}/bin/expect -c '{script}'"
 | |
| 
 | |
| 
 | |
|     server.start()
 | |
|     server.wait_for_unit("default.target")
 | |
| 
 | |
|     with subtest("slapd: auth as database admin with SASL and check a POSIX account"):
 | |
|         server.succeed(
 | |
|             'test "$(ldapsearch -LLL -H ldapi:// -Y EXTERNAL '
 | |
|             + "-b 'uid=${ldapUser},ou=accounts,ou=posix,${dbSuffix}' "
 | |
|             + "-s base uidNumber | "
 | |
|             + "sed -ne 's/^uidNumber: \\(.*\\)/\\1/p')\" -eq ${toString ldapUserId}"
 | |
|         )
 | |
| 
 | |
|     with subtest("slapd: auth as database admin with password and check a POSIX account"):
 | |
|         server.succeed(
 | |
|             "test \"$(ldapsearch -LLL -H ldap://server -D 'cn=admin,${dbSuffix}' "
 | |
|             + "-w '${dbAdminPwd}' -b 'uid=${ldapUser},ou=accounts,ou=posix,${dbSuffix}' "
 | |
|             + "-s base uidNumber | "
 | |
|             + "sed -ne 's/^uidNumber: \\(.*\\)/\\1/p')\" -eq ${toString ldapUserId}"
 | |
|         )
 | |
| 
 | |
|     client1.start()
 | |
|     client1.wait_for_unit("default.target")
 | |
| 
 | |
|     with subtest("password: su with password to a POSIX account"):
 | |
|         client1.succeed(
 | |
|             expect_script(
 | |
|                 'spawn su "${ldapUser}"',
 | |
|                 'expect "Password:"',
 | |
|                 'send "${ldapUserPwd}\n"',
 | |
|                 'expect "*"',
 | |
|                 'send "whoami\n"',
 | |
|                 'expect -ex "${ldapUser}" {exit}',
 | |
|                 "exit 1",
 | |
|             )
 | |
|         )
 | |
| 
 | |
|     with subtest("password: change password of a POSIX account as root"):
 | |
|         client1.succeed(
 | |
|             "chpasswd <<<'${ldapUser}:new-password'",
 | |
|             expect_script(
 | |
|                 'spawn su "${ldapUser}"',
 | |
|                 'expect "Password:"',
 | |
|                 'send "new-password\n"',
 | |
|                 'expect "*"',
 | |
|                 'send "whoami\n"',
 | |
|                 'expect -ex "${ldapUser}" {exit}',
 | |
|                 "exit 1",
 | |
|             ),
 | |
|             "chpasswd <<<'${ldapUser}:${ldapUserPwd}'",
 | |
|         )
 | |
| 
 | |
|     with subtest("password: change password of a POSIX account from itself"):
 | |
|         client1.succeed(
 | |
|             "chpasswd <<<'${ldapUser}:${ldapUserPwd}' ",
 | |
|             expect_script(
 | |
|                 "spawn su --login ${ldapUser} -c passwd",
 | |
|                 'expect "Password: "',
 | |
|                 'send "${ldapUserPwd}\n"',
 | |
|                 'expect "(current) UNIX password: "',
 | |
|                 'send "${ldapUserPwd}\n"',
 | |
|                 'expect "New password: "',
 | |
|                 'send "new-password\n"',
 | |
|                 'expect "Retype new password: "',
 | |
|                 'send "new-password\n"',
 | |
|                 'expect "passwd: password updated successfully" {exit}',
 | |
|                 "exit 1",
 | |
|             ),
 | |
|             expect_script(
 | |
|                 'spawn su "${ldapUser}"',
 | |
|                 'expect "Password:"',
 | |
|                 'send "${ldapUserPwd}\n"',
 | |
|                 'expect "su: Authentication failure" {exit}',
 | |
|                 "exit 1",
 | |
|             ),
 | |
|             expect_script(
 | |
|                 'spawn su "${ldapUser}"',
 | |
|                 'expect "Password:"',
 | |
|                 'send "new-password\n"',
 | |
|                 'expect "*"',
 | |
|                 'send "whoami\n"',
 | |
|                 'expect -ex "${ldapUser}" {exit}',
 | |
|                 "exit 1",
 | |
|             ),
 | |
|             "chpasswd <<<'${ldapUser}:${ldapUserPwd}'",
 | |
|         )
 | |
| 
 | |
|     client2.start()
 | |
|     client2.wait_for_unit("default.target")
 | |
| 
 | |
|     with subtest("NSS"):
 | |
|         client1.succeed(
 | |
|             "test \"$(id -u    '${ldapUser}')\" -eq ${toString ldapUserId}",
 | |
|             "test \"$(id -u -n '${ldapUser}')\" =  '${ldapUser}'",
 | |
|             "test \"$(id -g    '${ldapUser}')\" -eq ${toString ldapGroupId}",
 | |
|             "test \"$(id -g -n '${ldapUser}')\" =  '${ldapGroup}'",
 | |
|             "test \"$(id -u    '${ldapUser}')\" -eq ${toString ldapUserId}",
 | |
|             "test \"$(id -u -n '${ldapUser}')\" =  '${ldapUser}'",
 | |
|             "test \"$(id -g    '${ldapUser}')\" -eq ${toString ldapGroupId}",
 | |
|             "test \"$(id -g -n '${ldapUser}')\" =  '${ldapGroup}'",
 | |
|         )
 | |
| 
 | |
|     with subtest("PAM"):
 | |
|         client1.succeed(
 | |
|             "echo ${ldapUserPwd} | su -l '${ldapUser}' -c true",
 | |
|             "echo ${ldapUserPwd} | su -l '${ldapUser}' -c true",
 | |
|         )
 | |
|   '';
 | |
| })
 | 
