diff --git a/UPGRADE-7.2.md b/UPGRADE-7.2.md index d4a43d9bc08df..14645864fd01b 100644 --- a/UPGRADE-7.2.md +++ b/UPGRADE-7.2.md @@ -29,6 +29,11 @@ FrameworkBundle * [BC BREAK] The `secrets:decrypt-to-local` command terminates with a non-zero exit code when a secret could not be read +Ldap +---- + + * Add methods for `saslBind()` and `whoami()` to `ConnectionInterface` and `LdapInterface` + Messenger --------- diff --git a/src/Symfony/Component/Ldap/Adapter/ConnectionInterface.php b/src/Symfony/Component/Ldap/Adapter/ConnectionInterface.php index aa575940340b3..d320849e8fef3 100644 --- a/src/Symfony/Component/Ldap/Adapter/ConnectionInterface.php +++ b/src/Symfony/Component/Ldap/Adapter/ConnectionInterface.php @@ -14,9 +14,13 @@ use Symfony\Component\Ldap\Exception\AlreadyExistsException; use Symfony\Component\Ldap\Exception\ConnectionTimeoutException; use Symfony\Component\Ldap\Exception\InvalidCredentialsException; +use Symfony\Component\Ldap\Exception\LdapException; /** * @author Charles Sarrazin + * + * @method void saslBind(?string $dn = null, #[\SensitiveParameter] ?string $password = null, ?string $mech = null, ?string $realm = null, ?string $authcId = null, ?string $authzId = null, ?string $props = null) + * @method string whoami() */ interface ConnectionInterface { @@ -33,4 +37,19 @@ public function isBound(): bool; * @throws InvalidCredentialsException When the connection can't be created because of an LDAP_INVALID_CREDENTIALS error */ public function bind(?string $dn = null, #[\SensitiveParameter] ?string $password = null): void; + + /* + * Binds the connection against a user's DN and password using SASL. + * + * @throws LdapException When SASL support is not available + * @throws AlreadyExistsException When the connection can't be created because of an LDAP_ALREADY_EXISTS error + * @throws ConnectionTimeoutException When the connection can't be created because of an LDAP_TIMEOUT error + * @throws InvalidCredentialsException When the connection can't be created because of an LDAP_INVALID_CREDENTIALS error + */ + // public function saslBind(?string $dn = null, #[\SensitiveParameter] ?string $password = null, ?string $mech = null, ?string $realm = null, ?string $authcId = null, ?string $authzId = null, ?string $props = null): void; + + /* + * Return authenticated and authorized (for SASL) DN. + */ + // public function whoami(): string; } diff --git a/src/Symfony/Component/Ldap/Adapter/ExtLdap/Connection.php b/src/Symfony/Component/Ldap/Adapter/ExtLdap/Connection.php index 450bfac09ffd6..d2d096c4ba20e 100644 --- a/src/Symfony/Component/Ldap/Adapter/ExtLdap/Connection.php +++ b/src/Symfony/Component/Ldap/Adapter/ExtLdap/Connection.php @@ -70,21 +70,69 @@ public function bind(?string $dn = null, #[\SensitiveParameter] ?string $passwor if (false === @ldap_bind($this->connection, $dn, $password)) { $error = ldap_error($this->connection); - switch (ldap_errno($this->connection)) { - case self::LDAP_INVALID_CREDENTIALS: - throw new InvalidCredentialsException($error); - case self::LDAP_TIMEOUT: - throw new ConnectionTimeoutException($error); - case self::LDAP_ALREADY_EXISTS: - throw new AlreadyExistsException($error); - } - ldap_get_option($this->connection, LDAP_OPT_DIAGNOSTIC_MESSAGE, $diagnostic_message); - throw new ConnectionException($error.' '.$diagnostic_message); + ldap_get_option($this->connection, LDAP_OPT_DIAGNOSTIC_MESSAGE, $diagnostic); + + throw match (ldap_errno($this->connection)) { + self::LDAP_INVALID_CREDENTIALS => new InvalidCredentialsException($error), + self::LDAP_TIMEOUT => new ConnectionTimeoutException($error), + self::LDAP_ALREADY_EXISTS => new AlreadyExistsException($error), + default => new ConnectionException($error.' '.$diagnostic), + }; + } + + $this->bound = true; + } + + /** + * @param string $password WARNING: When the LDAP server allows unauthenticated binds, a blank $password will always be valid + */ + public function saslBind(?string $dn = null, #[\SensitiveParameter] ?string $password = null, ?string $mech = null, ?string $realm = null, ?string $authcId = null, ?string $authzId = null, ?string $props = null): void + { + if (!\function_exists('ldap_sasl_bind')) { + throw new LdapException('The LDAP extension is missing SASL support.'); + } + + if (!$this->connection) { + $this->connect(); + } + + if (false === @ldap_sasl_bind($this->connection, $dn, $password, $mech, $realm, $authcId, $authzId, $props)) { + $error = ldap_error($this->connection); + ldap_get_option($this->connection, LDAP_OPT_DIAGNOSTIC_MESSAGE, $diagnostic); + + throw match (ldap_errno($this->connection)) { + self::LDAP_INVALID_CREDENTIALS => new InvalidCredentialsException($error), + self::LDAP_TIMEOUT => new ConnectionTimeoutException($error), + self::LDAP_ALREADY_EXISTS => new AlreadyExistsException($error), + default => new ConnectionException($error.' '.$diagnostic), + }; } $this->bound = true; } + /** + * ldap_exop_whoami accessor, returns authenticated DN. + */ + public function whoami(): string + { + if (false === $authzId = ldap_exop_whoami($this->connection)) { + throw new LdapException(ldap_error($this->connection)); + } + + $parts = explode(':', $authzId, 2); + if ('dn' !== $parts[0]) { + /* + * We currently do not handle u:login authzId, which + * would require a configuration-dependent LDAP search + * to be turned into a DN + */ + throw new LdapException(\sprintf('Unsupported authzId "%s".', $authzId)); + } + + return $parts[1]; + } + /** * @internal */ diff --git a/src/Symfony/Component/Ldap/CHANGELOG.md b/src/Symfony/Component/Ldap/CHANGELOG.md index 01f86bcafb888..61a11df2cafd0 100644 --- a/src/Symfony/Component/Ldap/CHANGELOG.md +++ b/src/Symfony/Component/Ldap/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.2 +--- + + * Add methods for `saslBind()` and `whoami()` to `ConnectionInterface` and `LdapInterface` + 7.1 --- diff --git a/src/Symfony/Component/Ldap/Ldap.php b/src/Symfony/Component/Ldap/Ldap.php index c390a2f5b7a8f..d10c46507e46f 100644 --- a/src/Symfony/Component/Ldap/Ldap.php +++ b/src/Symfony/Component/Ldap/Ldap.php @@ -32,6 +32,16 @@ public function bind(?string $dn = null, #[\SensitiveParameter] ?string $passwor $this->adapter->getConnection()->bind($dn, $password); } + public function saslBind(?string $dn = null, #[\SensitiveParameter] ?string $password = null, ?string $mech = null, ?string $realm = null, ?string $authcId = null, ?string $authzId = null, ?string $props = null): void + { + $this->adapter->getConnection()->saslBind($dn, $password, $mech, $realm, $authcId, $authzId, $props); + } + + public function whoami(): string + { + return $this->adapter->getConnection()->whoami(); + } + public function query(string $dn, string $query, array $options = []): QueryInterface { return $this->adapter->createQuery($dn, $query, $options); diff --git a/src/Symfony/Component/Ldap/LdapInterface.php b/src/Symfony/Component/Ldap/LdapInterface.php index da9dce8e4116d..8cfe8a4a2f7bc 100644 --- a/src/Symfony/Component/Ldap/LdapInterface.php +++ b/src/Symfony/Component/Ldap/LdapInterface.php @@ -16,9 +16,10 @@ use Symfony\Component\Ldap\Exception\ConnectionException; /** - * Ldap interface. - * * @author Charles Sarrazin + * + * @method void saslBind(?string $dn = null, #[\SensitiveParameter] ?string $password = null, ?string $mech = null, ?string $realm = null, ?string $authcId = null, ?string $authzId = null, ?string $props = null) + * @method string whoami() */ interface LdapInterface { @@ -26,12 +27,24 @@ interface LdapInterface public const ESCAPE_DN = 0x02; /** - * Return a connection bound to the ldap. + * Returns a connection bound to the ldap. * * @throws ConnectionException if dn / password could not be bound */ public function bind(?string $dn = null, #[\SensitiveParameter] ?string $password = null): void; + /** + * Returns a connection bound to the ldap using SASL. + * + * @throws ConnectionException if dn / password could not be bound + */ + // public function saslBind(?string $dn = null, #[\SensitiveParameter] ?string $password = null, ?string $mech = null, ?string $realm = null, ?string $authcId = null, ?string $authzId = null, ?string $props = null): void; + + /** + * Returns authenticated and authorized (for SASL) DN. + */ + // public function whoami(): string; + /** * Queries a ldap server for entries matching the given criteria. */ diff --git a/src/Symfony/Component/Ldap/Tests/Adapter/ExtLdap/AdapterTest.php b/src/Symfony/Component/Ldap/Tests/Adapter/ExtLdap/AdapterTest.php index 174483f7ba6e7..c436c39045c94 100644 --- a/src/Symfony/Component/Ldap/Tests/Adapter/ExtLdap/AdapterTest.php +++ b/src/Symfony/Component/Ldap/Tests/Adapter/ExtLdap/AdapterTest.php @@ -34,6 +34,17 @@ public function testLdapEscape() $this->assertEquals('\20foo\3dbar\0d(baz)*\20', $ldap->escape(" foo=bar\r(baz)* ", '', LdapInterface::ESCAPE_DN)); } + /** + * @group functional + */ + public function testSaslBind() + { + $ldap = new Adapter($this->getLdapConfig()); + + $ldap->getConnection()->saslBind('cn=admin,dc=symfony,dc=com', 'symfony'); + $this->assertEquals('cn=admin,dc=symfony,dc=com', $ldap->getConnection()->whoami()); + } + /** * @group functional */