From 891d20dfccb5582e54bbad12444cf16a71e2f8c4 Mon Sep 17 00:00:00 2001 From: David Coutadeur Date: Fri, 29 Mar 2024 19:36:18 +0100 Subject: [PATCH] fix new Ldap.php functions + add corresponding tests (#11) --- src/Ltb/Ldap.php | 29 ++-- tests/Ltb/LdapTest.php | 329 ++++++++++++++++++++++++++++++++++++- tests/Ltb/PasswordTest.php | 6 +- 3 files changed, 348 insertions(+), 16 deletions(-) diff --git a/src/Ltb/Ldap.php b/src/Ltb/Ldap.php index 5cc7084..52b2b7c 100644 --- a/src/Ltb/Ldap.php +++ b/src/Ltb/Ldap.php @@ -139,20 +139,21 @@ static function sorted_search($ldap, $ldap_base, $ldap_filter, $attributes, $sor /** * Gets the value of the password attribute - * @param \LDAP\Connection|array $ldap An LDAP\Connection instance, returned by ldap_connect() + * @param \LDAP\Connection $ldap An LDAP\Connection instance, returned by ldap_connect() * @param string $dn the dn of the user - * @param type $pwdattribute the Attribute that contains the password - * @return string the value of $pwdattribute + * @param string $pwdattribute the Attribute that contains the password + * @return array|false the values of the password, as returned by ldap_get_values */ - static function get_password_value($ldap, $dn, $pwdattribute): string { + static function get_password_values($ldap, $dn, $pwdattribute): array|false { $search_userpassword = \Ltb\PhpLDAP::ldap_read($ldap, $dn, "(objectClass=*)", array($pwdattribute)); if ($search_userpassword) { - return \Ltb\PhpLDAP::ldap_get_values($ldap, ldap_first_entry($ldap, $search_userpassword), $pwdattribute); + return \Ltb\PhpLDAP::ldap_get_values($ldap, \Ltb\PhpLDAP::ldap_first_entry($ldap, $search_userpassword), $pwdattribute); } + return false; } /** - * Changes the password of an user while binded as the user in an Active Directory + * Changes the password of a user while binded as the user in an Active Directory * @param \LDAP\Connection|array $ldap An LDAP\Connection instance, returned by ldap_connect() * @param string $dn the dn of the user * @param string $oldpassword the old password @@ -163,7 +164,7 @@ static function change_ad_password_as_user($ldap, $dn, $oldpassword, $password): # The AD password change procedure is modifying the attribute unicodePwd by # first deleting unicodePwd with the old password and them adding it with the # the new password - $oldpassword_hashed = make_ad_password($oldpassword); + $oldpassword_hashed = \Ltb\Password::make_ad_password($oldpassword); $modifications = array( array( @@ -175,12 +176,12 @@ static function change_ad_password_as_user($ldap, $dn, $oldpassword, $password): "attrib" => "unicodePwd", "modtype" => LDAP_MODIFY_BATCH_ADD, "values" => array($password), - ), + ) ); \Ltb\PhpLDAP::ldap_modify_batch($ldap, $dn, $modifications); - $error_code = ldap_errno($ldap); - $error_msg = ldap_error($ldap); + $error_code = \Ltb\PhpLDAP::ldap_errno($ldap); + $error_msg = \Ltb\PhpLDAP::ldap_error($ldap); return array($error_code, $error_msg); } @@ -226,10 +227,10 @@ static function change_password_with_exop($ldap, $dn, $oldpassword, $password, $ } /** - * Changes attributes (and password) using Password Policy Control + * Changes attributes (and possibly password) using Password Policy Control * @param \LDAP\Connection|array $ldap An LDAP\Connection instance, returned by ldap_connect() * @param string $dn the dn of the user - * @param array $userdata the array, containing the new (hashed) password + * @param array $userdata the array, containing the modifications * @return array 0: error_code, 1: error_msg, 2: ppolicy_error_code */ static function modify_attributes_using_ppolicy($ldap, $dn, $userdata): array { @@ -253,8 +254,8 @@ static function modify_attributes_using_ppolicy($ldap, $dn, $userdata): array { */ static function modify_attributes($ldap, $dn, $userdata): array { \Ltb\PhpLDAP::ldap_mod_replace($ldap, $dn, $userdata); - $error_code = ldap_errno($ldap); - $error_msg = ldap_error($ldap); + $error_code = \Ltb\PhpLDAP::ldap_errno($ldap); + $error_msg = \Ltb\PhpLDAP::ldap_error($ldap); return array($error_code, $error_msg); } diff --git a/tests/Ltb/LdapTest.php b/tests/Ltb/LdapTest.php index 8c85c93..045a5e2 100644 --- a/tests/Ltb/LdapTest.php +++ b/tests/Ltb/LdapTest.php @@ -220,7 +220,7 @@ public function test_sorted_search_with_sort_control(): void $this->assertEquals(0, $errno, "error code invalid while getting ldap_search sorted result"); $this->assertEquals('testcn2', $entries[0]['cn'][0], "error while getting ldap_search sorted result: first entry is not testcn2"); $this->assertEquals('testcn1', $entries[1]['cn'][0], "error while getting ldap_search sorted result: second entry is not testcn1"); - + } public function test_sorted_search_without_sort_control(): void @@ -316,4 +316,331 @@ public function test_sorted_search_without_sort_control(): void $this->assertEquals('testcn1', $entries[1]['cn'][0], "error while getting ldap_search sorted result: second entry is not testcn1"); } + + public function test_get_password_value(): void + { + + $ldap_connection = "ldap_connection"; + $dn = "uid=test,ou=people,dc=my-domain,dc=com"; + $pwdattribute = "userPassword"; + $expectedValues = [ + "count" => 1, + 0 => 'secret' + ]; + + $phpLDAPMock = Mockery::mock('overload:Ltb\PhpLDAP'); + + $phpLDAPMock->shouldreceive('ldap_read') + ->with($ldap_connection, $dn, '(objectClass=*)', [ $pwdattribute ]) + ->andReturn("ldap_result"); + + $phpLDAPMock->shouldreceive('ldap_first_entry') + ->with($ldap_connection, "ldap_result") + ->andReturn("result_entry"); + + $phpLDAPMock->shouldreceive('ldap_get_values') + ->with($ldap_connection, "result_entry", $pwdattribute) + ->andReturn($expectedValues); + + $values = Ltb\Ldap::get_password_values( + $ldap_connection, + $dn, + $pwdattribute + ); + + $this->assertEquals(1, $values['count'], "error while getting cardinal of password values in get_password_value"); + $this->assertEquals('secret', $values[0], "wrong password value in get_password_value"); + + } + + public function test_get_password_value_with_dummy_pwdattribute(): void + { + + $ldap_connection = "ldap_connection"; + $dn = "uid=test,ou=people,dc=my-domain,dc=com"; + $pwdattribute = false; + + $phpLDAPMock = Mockery::mock('overload:Ltb\PhpLDAP'); + + $phpLDAPMock->shouldreceive('ldap_read') + ->with($ldap_connection, $dn, '(objectClass=*)', [ $pwdattribute ]) + ->andReturn(false); + + $values = Ltb\Ldap::get_password_values( + $ldap_connection, + $dn, + $pwdattribute + ); + $this->assertFalse($values, 'Weird returned value in get_password_value while sending dummy $pwdattribute'); + + } + + /** runInSeparateProcess is needed for \Ltb\Password + * not interfering with other tests + * @runInSeparateProcess + */ + #[RunInSeparateProcess] + public function test_change_ad_password_as_user(): void + { + + $ldap_connection = "ldap_connection"; + $dn = "uid=test,ou=people,dc=my-domain,dc=com"; + $old_password = "old"; + $hased_old_password = "hashed"; + $new_password = "new"; + $modifications = array( + array( + "attrib" => "unicodePwd", + "modtype" => LDAP_MODIFY_BATCH_REMOVE, + "values" => array($hased_old_password), + ), + array( + "attrib" => "unicodePwd", + "modtype" => LDAP_MODIFY_BATCH_ADD, + "values" => array($new_password), + ) + ); + + $phpLDAPMock = Mockery::mock('overload:Ltb\PhpLDAP'); + + $phpLDAPMock->shouldreceive('ldap_modify_batch') + ->with($ldap_connection, $dn, $modifications) + ->andReturn(true); + + $phpLDAPMock->shouldreceive('ldap_errno') + ->with($ldap_connection) + ->andReturn(0); + + $phpLDAPMock->shouldreceive('ldap_error') + ->with($ldap_connection) + ->andReturn("ok"); + + + $passwordMock = Mockery::mock('overload:\Ltb\Password'); + + $passwordMock->shouldreceive('make_ad_password') + ->with($old_password) + ->andReturn($hased_old_password); + + + list($error_code, $error_msg) = + Ltb\Ldap::change_ad_password_as_user( + $ldap_connection, + $dn, + $old_password, + $new_password + ); + + $this->assertEquals(0, $error_code, 'Weird error code returned in change_ad_password_as_user'); + $this->assertEquals("ok", $error_msg, 'Weird msg returned in change_ad_password_as_user'); + + } + + + public function test_get_ppolicy_error_code(): void + { + // method get_ppolicy_error_code cannot be tested as it is protected (and Ldap class is final) + + $this->assertTrue(method_exists("\Ltb\Ldap",'get_ppolicy_error_code'), 'No method get_ppolicy_error_code in class'); + + } + + public function test_change_password_with_exop_noppolicy(): void + { + + $ldap_connection = "ldap_connection"; + $dn = "uid=test,ou=people,dc=my-domain,dc=com"; + $old_password = "old"; + $new_password = "new"; + $ppolicy = false; + $res = true; // ldap_exop_passwd result is string|bool (new password if omitted from args else true or false) + + $phpLDAPMock = Mockery::mock('overload:Ltb\PhpLDAP'); + + $phpLDAPMock->shouldreceive('ldap_exop_passwd') + ->with($ldap_connection, $dn, $old_password, $new_password) + ->andReturn($res); + + $phpLDAPMock->shouldreceive('ldap_errno') + ->with($ldap_connection) + ->andReturn(0); + + $phpLDAPMock->shouldreceive('ldap_error') + ->with($ldap_connection) + ->andReturn("ok"); + + list($error_code, $error_msg, $ppolicy_error_code) = + Ltb\Ldap::change_password_with_exop( + $ldap_connection, + $dn, + $old_password, + $new_password, + $ppolicy + ); + + $this->assertEquals(0, $error_code, 'Weird error code returned in change_password_with_exop'); + $this->assertEquals("ok", $error_msg, 'Weird msg returned in change_password_with_exop'); + $this->assertFalse($ppolicy_error_code, 'Weird ppolicy_error_code returned in change_password_with_exop'); + + } + + public function test_change_password_with_exop_ppolicy(): void + { + + $ldap_connection = "ldap_connection"; + $dn = "uid=test,ou=people,dc=my-domain,dc=com"; + $old_password = "old"; + $new_password = "new"; + $ppolicy = true; + $res = true; // ldap_exop_passwd result is string|bool (new password if omitted from args else true or false) + + $phpLDAPMock = Mockery::mock('overload:Ltb\PhpLDAP'); + + $phpLDAPMock->shouldreceive('ldap_exop_passwd') + ->with($ldap_connection, $dn, $old_password, $new_password, array()) + ->andReturn($res); + + $phpLDAPMock->shouldreceive('ldap_errno') + ->with($ldap_connection) + ->andReturn(0); + + $phpLDAPMock->shouldreceive('ldap_error') + ->with($ldap_connection) + ->andReturn("ok"); + + list($error_code, $error_msg, $ppolicy_error_code) = + Ltb\Ldap::change_password_with_exop( + $ldap_connection, + $dn, + $old_password, + $new_password, + $ppolicy + ); + + $this->assertEquals(0, $error_code, 'Weird error code returned in change_password_with_exop with policy'); + $this->assertEquals("ok", $error_msg, 'Weird msg returned in change_password_with_exop with policy'); + $this->assertFalse($ppolicy_error_code, 'Weird ppolicy_error_code returned in change_password_with_exop with policy'); + + } + + public function test_change_password_with_exop_ppolicy_fail(): void + { + + $ldap_connection = "ldap_connection"; + $dn = "uid=test,ou=people,dc=my-domain,dc=com"; + $old_password = "old"; + $new_password = "new"; + $ppolicy = true; + $res = false; // ldap_exop_passwd result is string|bool (new password if omitted from args else true or false) + + $phpLDAPMock = Mockery::mock('overload:Ltb\PhpLDAP'); + + $phpLDAPMock->shouldreceive('ldap_exop_passwd') + ->with($ldap_connection, $dn, $old_password, $new_password, array()) + ->andReturn($res); + + $phpLDAPMock->shouldreceive('ldap_errno') + ->with($ldap_connection) + ->andReturn(49); + + $phpLDAPMock->shouldreceive('ldap_error') + ->with($ldap_connection) + ->andReturn("Invalid credentials"); + + list($error_code, $error_msg, $ppolicy_error_code) = + Ltb\Ldap::change_password_with_exop( + $ldap_connection, + $dn, + $old_password, + $new_password, + $ppolicy + ); + + $this->assertEquals(49, $error_code, 'Weird error code returned in failing change_password_with_exop with policy'); + $this->assertEquals("Invalid credentials", $error_msg, 'Weird msg returned in failing change_password_with_exop with policy'); + $this->assertFalse($ppolicy_error_code, 'Weird ppolicy_error_code returned in failing change_password_with_exop with policy'); + + } + + public function test_modify_attributes_using_ppolicy(): void + { + + $ldap_connection = "ldap_connection"; + $dn = "uid=test,ou=people,dc=my-domain,dc=com"; + $userdata = [ + "mail" => [ 'test1@domain.com', 'test2@domain.com'], + "userPassword" => "secret", + "description" => array() + ]; + $ctrls = [['oid' => LDAP_CONTROL_PASSWORDPOLICYREQUEST]]; + $res = true; // result of ldap_mod_replace_ext operation + + $phpLDAPMock = Mockery::mock('overload:Ltb\PhpLDAP'); + + $phpLDAPMock->shouldreceive('ldap_mod_replace_ext') + ->with($ldap_connection, $dn, $userdata, $ctrls) + ->andReturn($res); + + $phpLDAPMock->shouldreceive('ldap_parse_result') + ->with($ldap_connection, $res, "", null, "", null, array()) + ->andReturn($res); + + + list($error_code, $error_msg, $ppolicy_error_code) = + Ltb\Ldap::modify_attributes_using_ppolicy( + $ldap_connection, + $dn, + $userdata + ); + + $this->assertEquals("", $error_code, 'Weird error code returned in modify_attributes_using_ppolicy'); + $this->assertEquals("", $error_msg, 'Weird msg returned in modify_attributes_using_ppolicy'); + $this->assertFalse($ppolicy_error_code, 'Weird ppolicy_error_code returned in modify_attributes_using_ppolicy'); + + } + + public function test_modify_attributes(): void + { + + $ldap_connection = "ldap_connection"; + $dn = "uid=test,ou=people,dc=my-domain,dc=com"; + $userdata = [ + "mail" => [ 'test1@domain.com', 'test2@domain.com'], + "userPassword" => "secret", + "description" => array() + ]; + $res = true; // result of ldap_mod_replace_ext operation + + $phpLDAPMock = Mockery::mock('overload:\Ltb\PhpLDAP'); + + $phpLDAPMock->shouldreceive('ldap_mod_replace') + ->with($ldap_connection, $dn, $userdata) + ->andReturn($res); + + $phpLDAPMock->shouldreceive('ldap_errno') + ->with($ldap_connection) + ->andReturn(0); + + $phpLDAPMock->shouldreceive('ldap_error') + ->with($ldap_connection) + ->andReturn("ok"); + + list($error_code, $error_msg) = + Ltb\Ldap::modify_attributes( + $ldap_connection, + $dn, + $userdata + ); + + $this->assertEquals(0, $error_code, 'Weird error code returned in modify_attributes'); + $this->assertEquals("ok", $error_msg, 'Weird msg returned in modify_attributes'); + + } + + public function setUp(): void + { + // Turn on error reporting + //error_reporting(E_ALL); + } + } diff --git a/tests/Ltb/PasswordTest.php b/tests/Ltb/PasswordTest.php index e994dec..ea0836e 100644 --- a/tests/Ltb/PasswordTest.php +++ b/tests/Ltb/PasswordTest.php @@ -2,7 +2,11 @@ require __DIR__ . '/../../vendor/autoload.php'; -final class PasswordTest extends \PHPUnit\Framework\TestCase { +use PHPUnit\Framework\TestCase; + +final class PasswordTest extends \Mockery\Adapter\Phpunit\MockeryTestCase +{ + function test_check_hash_algorithms() { $originalPassword = 'TestMe123!+*';