diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml index a808734..187fb35 100644 --- a/.github/workflows/coding-standards.yml +++ b/.github/workflows/coding-standards.yml @@ -30,4 +30,5 @@ jobs: uses: "ramsey/composer-install@v1" - name: "Run PHP_CodeSniffer" - run: "vendor/bin/phpcs -q --no-colors --report=checkstyle | cs2pr" + run: | + vendor/bin/phpcs -q --no-colors --report=checkstyle | cs2pr diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index f422cee..75325a2 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -50,18 +50,33 @@ jobs: services: ldap: - image: bitnami/openldap + image: bitnami/openldap:latest ports: - - 3389:3389 + - 1389:1389 + - 1636:1636 env: LDAP_ADMIN_USERNAME: admin LDAP_ADMIN_PASSWORD: a_great_password LDAP_ROOT: dc=local,dc=com - LDAP_PORT_NUMBER: 3389 + LDAP_PORT_NUMBER: 1389 LDAP_USERS: a LDAP_PASSWORDS: a + LDAP_ENABLE_TLS: yes + LDAP_LDAPS_PORT_NUMBER: 1636 + LDAP_TLS_VERIFY_CLIENT: try + LDAP_TLS_CERT_FILE: /opt/bitnami/openldap/certs/openldap.crt + LDAP_TLS_KEY_FILE: /opt/bitnami/openldap/certs/openldap.key + LDAP_TLS_CA_FILE: /opt/bitnami/openldap/certs/openldapCA.crt + volumes: + - ${{ github.workspace }}/Tests/certs:/opt/bitnami/openldap/certs + options: --name=ldaprecord steps: + # Required as ./Tests/certs is created during ldap service build. + - name: "Make current user owner of workspace" + run: | + sudo chown -R $USER:$USER ${{ github.workspace }} + - name: "Checkout" uses: "actions/checkout@v2" @@ -73,8 +88,17 @@ jobs: php-version: "${{ matrix.php-version }}" ini-values: "zend.assertions=1" + - name: "Generate certificates" + run: | + composer generate-certs + + - name: "Restart Docker" + run: | + docker restart ldaprecord + - name: "Validate composer files" - run: "composer validate --strict" + run: | + composer validate --strict - name: "Cache dependencies installed with composer" uses: "actions/cache@v2" @@ -82,8 +106,12 @@ jobs: path: "~/.composer/cache" key: php-${{ matrix.php-version }}-symfony-${{ matrix.symfony-require }}-composer-locked-${{ hashFiles('composer.lock') }} restore-keys: | + php-${{ matrix.php-version }}-symfony-${{ matrix.symfony-require }}-composer-locked-${{ hashFiles('composer.lock') }} php-${{ matrix.php-version }}-symfony-${{ matrix.symfony-require }}-composer-locked- + php-${{ matrix.php-version }}-symfony-${{ matrix.symfony-require }} + php-${{ matrix.php-version }}-symfony- php-${{ matrix.php-version }}- + php- - name: "Install dependencies with composer" env: @@ -93,4 +121,5 @@ jobs: composer update --no-interaction --no-progress ${{ matrix.composer-flags }} - name: "Run PHPUnit" - run: "vendor/bin/phpunit" + run: | + composer phpunit diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 703ff97..ecf01dd 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -30,7 +30,9 @@ jobs: uses: "ramsey/composer-install@v1" - name: "Run a static analysis with phpstan/phpstan" - run: "vendor/bin/phpstan analyse" + run: | + vendor/bin/phpstan analyse - name: "Run a static analysis with vimeo/psalm" - run: "vendor/bin/psalm --output-format=github" + run: | + vendor/bin/psalm --output-format=github diff --git a/.gitignore b/.gitignore index bcb722c..d560bc4 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,8 @@ package.tar /.psalm/ /.idea/ /.phpcs-cache +#/Tests/certs/*.crt +/Tests/certs/*.csr +#/Tests/certs/*.key +/Tests/certs/*.srl + diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 9fa95e2..61efd26 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -59,6 +59,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->defaultFalse() ->end() ->arrayNode('options') + ->useAttributeAsKey('name') ->arrayPrototype() ->children() ->scalarNode('name')->end() diff --git a/Tests/DependencyInjection/Iter8LdapRecordExtensionTest.php b/Tests/DependencyInjection/Iter8LdapRecordExtensionTest.php index 551def2..9d9188f 100644 --- a/Tests/DependencyInjection/Iter8LdapRecordExtensionTest.php +++ b/Tests/DependencyInjection/Iter8LdapRecordExtensionTest.php @@ -29,30 +29,39 @@ public function test_load_empty_configuration(): void { $this->expectException(InvalidConfigurationException::class); - $container = $this->createContainer(); - $container->registerExtension(new Iter8LdapRecordExtension()); - $container->loadFromExtension('iter8_ldap_record'); - $container->compile(); + $this->createContainerWithConfig([]); } public function test_load_valid_configuration(): void { - $container = $this->createContainer(); - $container->registerExtension(new Iter8LdapRecordExtension()); - $container->loadFromExtension('iter8_ldap_record', $this->baseConfig()); - $container->compile(); + $ldapConfig = $this->getLdapConfig(); + + $config = \array_merge( + $this->baseConfig(), + [ + 'hosts' => [$ldapConfig['host']], + 'port' => $ldapConfig['port'], + ] + ); + + $container = $this->createContainerWithConfig($config); self::assertTrue($container->getDefinition('iter8_ldap_record.connection')->isPublic()); } public function test_is_connected_with_auto_connect_disabled(): void { - $this->getLdapConfig(); + $ldapConfig = $this->getLdapConfig(); - $container = $this->createContainer(); - $container->registerExtension(new Iter8LdapRecordExtension()); - $container->loadFromExtension('iter8_ldap_record', $this->baseConfig()); - $container->compile(); + $config = \array_merge( + $this->baseConfig(), + [ + 'hosts' => [$ldapConfig['host']], + 'port' => $ldapConfig['port'], + ] + ); + + $container = $this->createContainerWithConfig($config); /** @var Connection $connection */ $connection = $container->get('iter8_ldap_record.connection'); @@ -62,35 +71,112 @@ public function test_is_connected_with_auto_connect_disabled(): void public function test_is_connected_with_auto_connect_enabled(): void { - $this->getLdapConfig(); + $ldapConfig = $this->getLdapConfig(); - $config = array_merge( + $config = \array_merge( $this->baseConfig(), - ['auto_connect' => true] + [ + 'hosts' => [$ldapConfig['host']], + 'port' => $ldapConfig['port'], + 'auto_connect' => true, + ] ); - $container = $this->createContainer(); - $container->registerExtension(new Iter8LdapRecordExtension()); - $container->loadFromExtension('iter8_ldap_record', $config); - $container->compile(); + $container = $this->createContainerWithConfig($config); + + /** @var Connection $connection */ + $connection = $container->get('iter8_ldap_record.connection'); + + self::assertTrue($connection->isConnected()); + } + + public function test_manual_connect_with_unsecured_connection(): void + { + $ldapConfig = $this->getLdapConfig(); + + $config = \array_merge( + $this->baseConfig(), + [ + 'hosts' => [$ldapConfig['host']], + 'port' => $ldapConfig['port'], + ] + ); + + $container = $this->createContainerWithConfig($config); + + /** @var Connection $connection */ + $connection = $container->get('iter8_ldap_record.connection'); + + $connection->connect(); + + self::assertTrue($connection->isConnected()); + } + + public function test_manual_connect_with_tls_connection(): void + { + $ldapConfig = $this->getLdapsConfig(); + + $config = \array_merge( + $this->baseConfig(), + [ + 'hosts' => [$ldapConfig['host']], + 'port' => $ldapConfig['port'], + 'use_tls' => true, + ] + ); + + $container = $this->createContainerWithConfig($config); /** @var Connection $connection */ $connection = $container->get('iter8_ldap_record.connection'); + $connection->connect(); + self::assertTrue($connection->isConnected()); } + public function test_can_find_user(): void + { + $ldapConfig = $this->getLdapConfig(); + + $config = \array_merge( + $this->baseConfig(), + [ + 'hosts' => [$ldapConfig['host']], + 'port' => $ldapConfig['port'], + ] + ); + + $container = $this->createContainerWithConfig($config); + + /** @var Connection $connection */ + $connection = $container->get('iter8_ldap_record.connection'); + + $results = $connection->query()->where('cn', '=', 'a')->get(); + + dump($results); + self::assertNotEmpty($results); + } + private function baseConfig(): array { return [ - 'hosts' => ['localhost'], 'base_dn' => 'dc=local,dc=com', 'username' => 'cn=admin,dc=local,dc=com', 'password' => 'a_great_password', - 'port' => 3389, ]; } + private function createContainerWithConfig(array $config): ContainerBuilder + { + $container = $this->createContainer(); + $container->registerExtension(new Iter8LdapRecordExtension()); + $container->loadFromExtension('iter8_ldap_record', $config); + $container->compile(); + + return $container; + } + private function createContainer(): ContainerBuilder { return new ContainerBuilder(new ParameterBag([ diff --git a/Tests/TestCase.php b/Tests/TestCase.php index c667bc0..e3c17fb 100644 --- a/Tests/TestCase.php +++ b/Tests/TestCase.php @@ -6,6 +6,9 @@ use PHPUnit\Framework\TestCase as PHPUnitTestCase; +/** + * @see https://github.com/symfony/symfony/blob/89fedfa/src/Symfony/Component/Ldap/Tests/LdapTestCase.php + */ class TestCase extends PHPUnitTestCase { protected function getLdapConfig(): array @@ -22,7 +25,40 @@ protected function getLdapConfig(): array return [ 'host' => getenv('LDAP_HOST'), - 'port' => getenv('LDAP_PORT'), + 'port' => (int) getenv('LDAP_PORT'), + ]; + } + + protected function getLdapsConfig(): array + { + putenv('TLS_REQCERT=allow'); + +// @ldap_set_option(null, \LDAP_OPT_DEBUG_LEVEL, 7); + @ldap_set_option(null, \LDAP_OPT_X_TLS_REQUIRE_CERT, \LDAP_OPT_X_TLS_ALLOW); + /** @var resource|null $h */ + $h = @ldap_connect((string) getenv('LDAP_HOST'), (int) getenv('LDAPS_PORT')); + @ldap_set_option($h, \LDAP_OPT_PROTOCOL_VERSION, 3); + @ldap_set_option($h, \LDAP_OPT_REFERRALS, 0); + if (\is_resource($h)) { + @ldap_get_option($h, \LDAP_OPT_DIAGNOSTIC_MESSAGE, $extendedError); + @ldap_start_tls($h); + } + + if (!\is_resource($h) || !@ldap_bind($h)) { +// dump(@ldap_error($h)); +// dump($extendedError); + self::markTestSkipped(\sprintf( + 'No server is listening on LDAP_HOST:LDAPS_PORT (%s:%s)', + getenv('LDAP_HOST'), + getenv('LDAPS_PORT') + )); + } + + ldap_unbind($h); + + return [ + 'host' => getenv('LDAP_HOST'), + 'port' => (int) getenv('LDAPS_PORT'), ]; } } diff --git a/Tests/certs/generate.sh b/Tests/certs/generate.sh new file mode 100755 index 0000000..5d599c6 --- /dev/null +++ b/Tests/certs/generate.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +set -e + +SCRIPT_PATH=$(dirname "$(realpath "$0")") + +# Create a root CA signing key. +openssl genrsa -out "${SCRIPT_PATH}/openldapCA.key" 4096 + +# Now create and self-sign the root CA certificate. +openssl req -x509 -new -nodes -key "${SCRIPT_PATH}/openldapCA.key" -sha256 -days 3650 -subj "/CN=localhostCA" -out "${SCRIPT_PATH}/openldapCA.crt" + +# Generate the LDAP server key. +openssl genrsa -out "${SCRIPT_PATH}/openldap.key" 2048 + +# Now create the CSR for the LDAP server certificate so we can sign it with our root CA. +openssl req -new -sha256 -key "${SCRIPT_PATH}/openldap.key" -subj "/CN=localhost" -out "${SCRIPT_PATH}/openldap.csr" + +# Finally, sign the LDAP server CSR with our root CA so it's ready to use. +openssl x509 -req -in "${SCRIPT_PATH}/openldap.csr" -CA "${SCRIPT_PATH}/openldapCA.crt" -CAkey "${SCRIPT_PATH}/openldapCA.key" -CAcreateserial -out "${SCRIPT_PATH}/openldap.crt" -sha256 -days 3650 + +# Remove the CSR as it's no longer needed. +rm "${SCRIPT_PATH}/openldap.csr" + +exit 0 diff --git a/composer.json b/composer.json index 9d3f13b..f445d18 100644 --- a/composer.json +++ b/composer.json @@ -65,6 +65,7 @@ "phpstan": "phpstan analyze", "phpstan-max": "@phpstan --level=max", "phpunit": "phpunit", - "psalm": "psalm --show-info=true" + "psalm": "psalm --show-info=true", + "generate-certs": "./Tests/certs/generate.sh" } } diff --git a/docker-compose.yml b/docker-compose.yml index 568296a..ccc1e04 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +1,24 @@ -version: '2' +version: '3.8' services: ldap: - image: bitnami/openldap + image: bitnami/openldap:latest + network_mode: "bridge" ports: - - 3389:3389 + - 1389:1389 + - 1636:1636 environment: - LDAP_ADMIN_USERNAME=admin - LDAP_ADMIN_PASSWORD=a_great_password - LDAP_USERS=a - LDAP_PASSWORDS=a - LDAP_ROOT=dc=local,dc=com - - LDAP_PORT_NUMBER=3389 + - LDAP_PORT_NUMBER=1389 + - LDAP_ENABLE_TLS=yes + - LDAP_LDAPS_PORT_NUMBER=1636 + - LDAP_TLS_VERIFY_CLIENT=try + - LDAP_TLS_CERT_FILE=/opt/bitnami/openldap/certs/openldap.crt + - LDAP_TLS_KEY_FILE=/opt/bitnami/openldap/certs/openldap.key + - LDAP_TLS_CA_FILE=/opt/bitnami/openldap/certs/openldapCA.crt + volumes: + - ./Tests/certs:/opt/bitnami/openldap/certs diff --git a/phpcs.xml.dist b/phpcs.xml.dist index ec27d43..49c2517 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -48,6 +48,10 @@ Tests + + Tests + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index b3988ba..fed6ae8 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -11,7 +11,8 @@ - + +