]> BookStack Code Mirror - bookstack/blob - app/Auth/Access/Ldap/LdapConnectionManager.php
Added more complexity in an attempt to make ldap host failover fit
[bookstack] / app / Auth / Access / Ldap / LdapConnectionManager.php
1 <?php
2
3 namespace BookStack\Auth\Access\Ldap;
4
5 use BookStack\Exceptions\LdapException;
6 use BookStack\Exceptions\LdapFailedBindException;
7 use ErrorException;
8 use Illuminate\Support\Facades\Log;
9
10 class LdapConnectionManager
11 {
12     protected array $connectionCache = [];
13
14     /**
15      * Attempt to start and bind to a new LDAP connection as the configured LDAP system user.
16      */
17     public function startSystemBind(LdapConfig $config): LdapConnection
18     {
19         // Incoming options are string|false
20         $dn = $config->get('dn');
21         $pass = $config->get('pass');
22
23         $isAnonymous = ($dn === false || $pass === false);
24
25         try {
26             return $this->startBind($dn ?: null, $pass ?: null, $config);
27         } catch (LdapFailedBindException $exception) {
28             $msg = ($isAnonymous ? trans('errors.ldap_fail_anonymous') : trans('errors.ldap_fail_authed'));
29             throw new LdapFailedBindException($msg);
30         }
31     }
32
33     /**
34      * Attempt to start and bind to a new LDAP connection.
35      * Will attempt against multiple defined fail-over hosts if set in the provided config.
36      *
37      * Throws a LdapFailedBindException error if the bind connected but failed.
38      * Otherwise, generic LdapException errors would be thrown.
39      *
40      * @throws LdapException
41      */
42     public function startBind(?string $dn, ?string $password, LdapConfig $config): LdapConnection
43     {
44         // Check LDAP extension in installed
45         if (!function_exists('ldap_connect') && config('app.env') !== 'testing') {
46             throw new LdapException(trans('errors.ldap_extension_not_installed'));
47         }
48
49         // Disable certificate verification.
50         // This option works globally and must be set before a connection is created.
51         if ($config->get('tls_insecure')) {
52             LdapConnection::setGlobalOption(LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER);
53         }
54
55         $serverDetails = $config->getServers();
56         $lastException = null;
57
58         foreach ($serverDetails as $server) {
59             try {
60                 $connection = $this->startServerConnection($server['host'], $server['port'], $config);
61             } catch (LdapException $exception) {
62                 $lastException = $exception;
63                 continue;
64             }
65
66             try {
67                 $bound = $connection->bind($dn, $password);
68                 if (!$bound) {
69                     throw new LdapFailedBindException('Failed to perform LDAP bind');
70                 }
71             } catch (ErrorException $exception) {
72                 Log::error('LDAP bind error: ' . $exception->getMessage());
73                 $lastException = new LdapException('Encountered error during LDAP bind');
74                 continue;
75             }
76
77             $this->connectionCache[$server['host'] . ':' . $server['port']] = $connection;
78             return $connection;
79         }
80
81         throw $lastException;
82     }
83
84     /**
85      * Attempt to start a server connection from the provided details.
86      * @throws LdapException
87      */
88     protected function startServerConnection(string $host, int $port, LdapConfig $config): LdapConnection
89     {
90         if (isset($this->connectionCache[$host . ':' . $port])) {
91             return $this->connectionCache[$host . ':' . $port];
92         }
93
94         /** @var LdapConnection $ldapConnection */
95         $ldapConnection = app()->make(LdapConnection::class, [$host, $port]);
96
97         if (!$ldapConnection) {
98             throw new LdapException(trans('errors.ldap_cannot_connect'));
99         }
100
101         // Set any required options
102         if ($config->get('version')) {
103             $ldapConnection->setVersion($config->get('version'));
104         }
105
106         // Start and verify TLS if it's enabled
107         if ($config->get('start_tls')) {
108             try {
109                 $tlsStarted = $ldapConnection->startTls();
110             } catch (ErrorException $exception) {
111                 $tlsStarted = false;
112             }
113
114             if (!$tlsStarted) {
115                 throw new LdapException('Could not start TLS connection');
116             }
117         }
118
119         return $ldapConnection;
120     }
121 }