From 8e8b1068ff917e03ead39bdb121db07631b22b98 Mon Sep 17 00:00:00 2001 From: Brady Wetherington Date: Mon, 27 Nov 2023 14:50:43 +0000 Subject: [PATCH 1/3] Beginnings of LDAP test suite --- composer.json | 1 + composer.lock | 209 ++++++++++++++++++++++++++++++++++++- tests/Support/Settings.php | 12 +++ tests/Unit/LdapTest.php | 28 +++++ 4 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 tests/Unit/LdapTest.php diff --git a/composer.json b/composer.json index 020b2f9ca7..58c2b6bedc 100644 --- a/composer.json +++ b/composer.json @@ -80,6 +80,7 @@ "mockery/mockery": "^1.4", "nunomaduro/larastan": "^1.0", "nunomaduro/phpinsights": "^2.7", + "php-mock/php-mock-phpunit": "^2.8", "phpunit/php-token-stream": "^3.1", "phpunit/phpunit": "^9.0", "squizlabs/php_codesniffer": "^3.5", diff --git a/composer.lock b/composer.lock index 7ca88da10f..b1fef91963 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "348f96db24a0f8dfb595ee38b38b34eb", + "content-hash": "f4f3b6b02d044ed3e54cdd509b01c3dc", "packages": [ { "name": "alek13/slack", @@ -14100,6 +14100,213 @@ }, "time": "2022-02-21T01:04:05+00:00" }, + { + "name": "php-mock/php-mock", + "version": "2.4.1", + "source": { + "type": "git", + "url": "https://github.com/php-mock/php-mock.git", + "reference": "6240b6f0a76d7b9d1ee4d70e686a7cc711619a9d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-mock/php-mock/zipball/6240b6f0a76d7b9d1ee4d70e686a7cc711619a9d", + "reference": "6240b6f0a76d7b9d1ee4d70e686a7cc711619a9d", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0 || ^8.0", + "phpunit/php-text-template": "^1 || ^2 || ^3" + }, + "replace": { + "malkusch/php-mock": "*" + }, + "require-dev": { + "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.0 || ^9.0 || ^10.0", + "squizlabs/php_codesniffer": "^3.5" + }, + "suggest": { + "php-mock/php-mock-phpunit": "Allows integration into PHPUnit testcase with the trait PHPMock." + }, + "type": "library", + "autoload": { + "files": [ + "autoload.php" + ], + "psr-4": { + "phpmock\\": [ + "classes/", + "tests/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "WTFPL" + ], + "authors": [ + { + "name": "Markus Malkusch", + "email": "markus@malkusch.de", + "homepage": "http://markus.malkusch.de", + "role": "Developer" + } + ], + "description": "PHP-Mock can mock built-in PHP functions (e.g. time()). PHP-Mock relies on PHP's namespace fallback policy. No further extension is needed.", + "homepage": "https://github.com/php-mock/php-mock", + "keywords": [ + "BDD", + "TDD", + "function", + "mock", + "stub", + "test", + "test double", + "testing" + ], + "support": { + "issues": "https://github.com/php-mock/php-mock/issues", + "source": "https://github.com/php-mock/php-mock/tree/2.4.1" + }, + "funding": [ + { + "url": "https://github.com/michalbundyra", + "type": "github" + } + ], + "time": "2023-06-12T20:48:52+00:00" + }, + { + "name": "php-mock/php-mock-integration", + "version": "2.2.1", + "source": { + "type": "git", + "url": "https://github.com/php-mock/php-mock-integration.git", + "reference": "04f4a8d5442ca457b102b5204673f77323e3edb5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-mock/php-mock-integration/zipball/04f4a8d5442ca457b102b5204673f77323e3edb5", + "reference": "04f4a8d5442ca457b102b5204673f77323e3edb5", + "shasum": "" + }, + "require": { + "php": ">=5.6", + "php-mock/php-mock": "^2.4", + "phpunit/php-text-template": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpunit/phpunit": "^5.7.27 || ^6 || ^7 || ^8 || ^9 || ^10" + }, + "type": "library", + "autoload": { + "psr-4": { + "phpmock\\integration\\": "classes/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "WTFPL" + ], + "authors": [ + { + "name": "Markus Malkusch", + "email": "markus@malkusch.de", + "homepage": "http://markus.malkusch.de", + "role": "Developer" + } + ], + "description": "Integration package for PHP-Mock", + "homepage": "https://github.com/php-mock/php-mock-integration", + "keywords": [ + "BDD", + "TDD", + "function", + "mock", + "stub", + "test", + "test double" + ], + "support": { + "issues": "https://github.com/php-mock/php-mock-integration/issues", + "source": "https://github.com/php-mock/php-mock-integration/tree/2.2.1" + }, + "funding": [ + { + "url": "https://github.com/michalbundyra", + "type": "github" + } + ], + "time": "2023-02-13T09:51:29+00:00" + }, + { + "name": "php-mock/php-mock-phpunit", + "version": "2.8.0", + "source": { + "type": "git", + "url": "https://github.com/php-mock/php-mock-phpunit.git", + "reference": "56edee85ad3232caa0202f98f2a3c899ab16bdb7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-mock/php-mock-phpunit/zipball/56edee85ad3232caa0202f98f2a3c899ab16bdb7", + "reference": "56edee85ad3232caa0202f98f2a3c899ab16bdb7", + "shasum": "" + }, + "require": { + "php": ">=7", + "php-mock/php-mock-integration": "^2.2.1", + "phpunit/phpunit": "^6 || ^7 || ^8 || ^9 || ^10.0.17" + }, + "require-dev": { + "mockery/mockery": "^1.3.6" + }, + "type": "library", + "autoload": { + "files": [ + "autoload.php" + ], + "psr-4": { + "phpmock\\phpunit\\": "classes/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "WTFPL" + ], + "authors": [ + { + "name": "Markus Malkusch", + "email": "markus@malkusch.de", + "homepage": "http://markus.malkusch.de", + "role": "Developer" + } + ], + "description": "Mock built-in PHP functions (e.g. time()) with PHPUnit. This package relies on PHP's namespace fallback policy. No further extension is needed.", + "homepage": "https://github.com/php-mock/php-mock-phpunit", + "keywords": [ + "BDD", + "TDD", + "function", + "mock", + "phpunit", + "stub", + "test", + "test double", + "testing" + ], + "support": { + "issues": "https://github.com/php-mock/php-mock-phpunit/issues", + "source": "https://github.com/php-mock/php-mock-phpunit/tree/2.8.0" + }, + "funding": [ + { + "url": "https://github.com/michalbundyra", + "type": "github" + } + ], + "time": "2023-10-30T07:06:12+00:00" + }, { "name": "php-parallel-lint/php-parallel-lint", "version": "v1.3.2", diff --git a/tests/Support/Settings.php b/tests/Support/Settings.php index 17f8af23d2..0ca05a4752 100644 --- a/tests/Support/Settings.php +++ b/tests/Support/Settings.php @@ -3,6 +3,7 @@ namespace Tests\Support; use App\Models\Setting; +use Illuminate\Support\Facades\Crypt; class Settings { @@ -67,6 +68,17 @@ class Settings } + public function enableLdap(): Settings + { + return $this->update([ + 'ldap_enabled' => 1, + 'ldap_server' => 'ldaps://ldap.example.com', + 'ldap_uname' => 'fake_username', + 'ldap_pword' => Crypt::encrypt("fake_password"), + 'ldap_basedn' => 'CN=Users,DC=ad,DC=example,Dc=com' + ]); + } + /** * @param array $attributes Attributes to modify in the application's settings. */ diff --git a/tests/Unit/LdapTest.php b/tests/Unit/LdapTest.php new file mode 100644 index 0000000000..7b94f1aac5 --- /dev/null +++ b/tests/Unit/LdapTest.php @@ -0,0 +1,28 @@ +settings->enableLdap(); + + $ldap_connect = $this->getFunctionMock("App\\Models", "ldap_connect"); + $ldap_connect->expects($this->once())->willReturn('hello'); + + $ldap_set_option = $this->getFunctionMock("App\\Models", "ldap_set_option"); + $ldap_set_option->expects($this->exactly(3)); + + + $blah = Ldap::connectToLdap(); + $this->assertEquals('hello',$blah,"LDAP_connect should return 'hello'"); + } +} From 16da994e28649896ee8369abd8a5754852d30217 Mon Sep 17 00:00:00 2001 From: Brady Wetherington Date: Thu, 30 Nov 2023 14:00:20 +0000 Subject: [PATCH 2/3] Add LDAP as 'recommended' requirement; flesh out LDAP tests --- composer.json | 3 ++ tests/Support/Settings.php | 22 +++++++++++ tests/Unit/LdapTest.php | 80 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+) diff --git a/composer.json b/composer.json index 58c2b6bedc..2a456999e9 100644 --- a/composer.json +++ b/composer.json @@ -74,6 +74,9 @@ "unicodeveloper/laravel-password": "^1.0", "watson/validating": "^6.1" }, + "suggest": { + "ext-ldap": "*" + }, "require-dev": { "brianium/paratest": "^6.6", "fakerphp/faker": "^1.16", diff --git a/tests/Support/Settings.php b/tests/Support/Settings.php index 0ca05a4752..30fe8c7017 100644 --- a/tests/Support/Settings.php +++ b/tests/Support/Settings.php @@ -79,6 +79,28 @@ class Settings ]); } + public function enableAnonymousLdap(): Settings + { + return $this->update([ + 'ldap_enabled' => 1, + 'ldap_server' => 'ldaps://ldap.example.com', +// 'ldap_uname' => 'fake_username', + 'ldap_pword' => Crypt::encrypt("fake_password"), + 'ldap_basedn' => 'CN=Users,DC=ad,DC=example,Dc=com' + ]); + } + + public function enableBadPasswordLdap(): Settings + { + return $this->update([ + 'ldap_enabled' => 1, + 'ldap_server' => 'ldaps://ldap.example.com', + 'ldap_uname' => 'fake_username', + 'ldap_pword' => "badly_encrypted_password!", + 'ldap_basedn' => 'CN=Users,DC=ad,DC=example,Dc=com' + ]); + } + /** * @param array $attributes Attributes to modify in the application's settings. */ diff --git a/tests/Unit/LdapTest.php b/tests/Unit/LdapTest.php index 7b94f1aac5..78e829c6e9 100644 --- a/tests/Unit/LdapTest.php +++ b/tests/Unit/LdapTest.php @@ -3,6 +3,7 @@ namespace Tests\Unit; use App\Models\Ldap; +use Exception; use Tests\Support\InteractsWithSettings; use Tests\TestCase; @@ -25,4 +26,83 @@ class LdapTest extends TestCase $blah = Ldap::connectToLdap(); $this->assertEquals('hello',$blah,"LDAP_connect should return 'hello'"); } + + // other test cases - with/without client-side certs? + // with/without LDAP version 3? + // with/without ignore cert validation? + // test (and mock) ldap_start_tls() ? + + public function testBindAdmin() + { + $this->settings->enableLdap(); + $this->getFunctionMock("App\\Models", "ldap_bind")->expects($this->once())->willReturn(true); + $this->assertNull(Ldap::bindAdminToLdap("dummy")); + } + + public function testBindBad() + { + $this->settings->enableLdap(); + $this->getFunctionMock("App\\Models", "ldap_bind")->expects($this->once())->willReturn(false); + $this->getFunctionMock("App\\Models","ldap_error")->expects($this->once())->willReturn("exception"); + $this->expectExceptionMessage("Could not bind to LDAP:"); + + $this->assertNull(Ldap::bindAdminToLdap("dummy")); + } + // other test cases - test donked password? + + public function testAnonymousBind() + { + //todo - would be nice to introspect somehow to make sure the right parameters were passed? + $this->settings->enableAnonymousLdap(); + $this->getFunctionMock("App\\Models", "ldap_bind")->expects($this->once())->willReturn(true); + $this->assertNull(Ldap::bindAdminToLdap("dummy")); + } + + public function testBadAnonymousBind() + { + $this->settings->enableAnonymousLdap(); + $this->getFunctionMock("App\\Models", "ldap_bind")->expects($this->once())->willReturn(false); + $this->getFunctionMock("App\\Models","ldap_error")->expects($this->once())->willReturn("exception"); + $this->expectExceptionMessage("Could not bind to LDAP:"); + + $this->assertNull(Ldap::bindAdminToLdap("dummy")); + } + + public function testBadEncryptedPassword() + { + $this->settings->enableBadPasswordLdap(); + + $this->expectExceptionMessage("Your app key has changed"); + $this->assertNull(Ldap::bindAdminToLdap("dummy")); + } + + public function testFindAndBind() + { + $this->settings->enableLdap(); + + $ldap_connect = $this->getFunctionMock("App\\Models", "ldap_connect"); + $ldap_connect->expects($this->once())->willReturn('hello'); + + $ldap_set_option = $this->getFunctionMock("App\\Models", "ldap_set_option"); + $ldap_set_option->expects($this->exactly(3)); + + $this->getFunctionMock("App\\Models", "ldap_bind")->expects($this->once())->willReturn(true); + + $this->getFunctionMock("App\\Models", "ldap_search")->expects($this->once())->willReturn(true); + + $this->getFunctionMock("App\\Models", "ldap_first_entry")->expects($this->once())->willReturn(true); + + $this->getFunctionMock("App\\Models", "ldap_get_attributes")->expects($this->once())->willReturn( + [ + "count" => 1, + 0 => [ + 'sn' => 'Surname', + 'firstName' => 'FirstName' + ] + ] + ); + + $results = Ldap::findAndBindUserLdap("username","password"); + $this->assertEqualsCanonicalizing(["count" =>1,0 =>['sn' => 'Surname','firstname' => 'FirstName']],$results); + } } From 5a60df55d1136d014e912d7c7c44e083097f1ff1 Mon Sep 17 00:00:00 2001 From: Brady Wetherington Date: Thu, 30 Nov 2023 16:09:37 +0000 Subject: [PATCH 3/3] Add more LDAP tests - including pagination(!) --- tests/Unit/LdapTest.php | 102 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/tests/Unit/LdapTest.php b/tests/Unit/LdapTest.php index 78e829c6e9..bae4f3ff4c 100644 --- a/tests/Unit/LdapTest.php +++ b/tests/Unit/LdapTest.php @@ -105,4 +105,106 @@ class LdapTest extends TestCase $results = Ldap::findAndBindUserLdap("username","password"); $this->assertEqualsCanonicalizing(["count" =>1,0 =>['sn' => 'Surname','firstname' => 'FirstName']],$results); } + + public function testFindAndBindBadPassword() + { + $this->settings->enableLdap(); + + $ldap_connect = $this->getFunctionMock("App\\Models", "ldap_connect"); + $ldap_connect->expects($this->once())->willReturn('hello'); + + $ldap_set_option = $this->getFunctionMock("App\\Models", "ldap_set_option"); + $ldap_set_option->expects($this->exactly(3)); + + // note - we return FALSE first, to simulate a bad-bind, then TRUE the second time to simulate a successful admin bind + $this->getFunctionMock("App\\Models", "ldap_bind")->expects($this->exactly(2))->willReturn(false, true); + +// $this->getFunctionMock("App\\Models","ldap_error")->expects($this->once())->willReturn("exception"); + + +// $this->expectExceptionMessage("exception"); + $results = Ldap::findAndBindUserLdap("username","password"); + $this->assertFalse($results); + } + + public function testFindAndBindCannotFindSelf() + { + $this->settings->enableLdap(); + + $ldap_connect = $this->getFunctionMock("App\\Models", "ldap_connect"); + $ldap_connect->expects($this->once())->willReturn('hello'); + + $ldap_set_option = $this->getFunctionMock("App\\Models", "ldap_set_option"); + $ldap_set_option->expects($this->exactly(3)); + + $this->getFunctionMock("App\\Models", "ldap_bind")->expects($this->once())->willReturn(true); + + $this->getFunctionMock("App\\Models", "ldap_search")->expects($this->once())->willReturn(false); + + $this->expectExceptionMessage("Could not search LDAP:"); + $results = Ldap::findAndBindUserLdap("username","password"); + $this->assertFalse($results); + } + + //maybe should do an AD test as well? + + public function testFindLdapUsers() + { + $this->settings->enableLdap(); + + $ldap_connect = $this->getFunctionMock("App\\Models", "ldap_connect"); + $ldap_connect->expects($this->once())->willReturn('hello'); + + $ldap_set_option = $this->getFunctionMock("App\\Models", "ldap_set_option"); + $ldap_set_option->expects($this->exactly(3)); + + $this->getFunctionMock("App\\Models", "ldap_bind")->expects($this->once())->willReturn(true); + + $this->getFunctionMock("App\\Models", "ldap_search")->expects($this->once())->willReturn(["stuff"]); + + $this->getFunctionMock("App\\Models", "ldap_parse_result")->expects($this->once())->willReturn(true); + + $this->getFunctionMock("App\\Models", "ldap_get_entries")->expects($this->once())->willReturn(["count" => 1]); + + $results = Ldap::findLdapUsers(); + + $this->assertEqualsCanonicalizing(["count" => 1], $results); + } + + public function testFindLdapUsersPaginated() + { + $this->settings->enableLdap(); + + $ldap_connect = $this->getFunctionMock("App\\Models", "ldap_connect"); + $ldap_connect->expects($this->once())->willReturn('hello'); + + $ldap_set_option = $this->getFunctionMock("App\\Models", "ldap_set_option"); + $ldap_set_option->expects($this->exactly(3)); + + $this->getFunctionMock("App\\Models", "ldap_bind")->expects($this->once())->willReturn(true); + + $this->getFunctionMock("App\\Models", "ldap_search")->expects($this->exactly(2))->willReturn(["stuff"]); + + $this->getFunctionMock("App\\Models", "ldap_parse_result")->expects($this->exactly(2))->willReturnCallback( + function ($ldapconn, $search_results, $errcode , $matcheddn , $errmsg , $referrals, &$controls) { + static $count = 0; + if($count == 0) { + $count++; + $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'] = "cookie"; + return ["count" => 1]; + } else { + $controls = []; + return ["count" => 1]; + } + + } + ); + + $this->getFunctionMock("App\\Models", "ldap_get_entries")->expects($this->exactly(2))->willReturn(["count" => 1]); + + $results = Ldap::findLdapUsers(); + + $this->assertEqualsCanonicalizing(["count" => 2], $results); + } + }