]> BookStack Code Mirror - bookstack/blob - app/Auth/Access/Ldap/LdapConnectionManager.php
e5720a49a267a19e62119cd7f21f0a0fbd5e6117
[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(array $config): LdapConnection
18     {
19         // Incoming options are string|false
20         $dn = $config['dn'];
21         $pass = $config['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, array $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['tls_insecure']) {
52             LdapConnection::setGlobalOption(LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER);
53         }
54
55         $serverDetails = $this->parseMultiServerString($config['server']);
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, array $config): LdapConnection
89     {
90         if (isset($this->connectionCache[$host . ':' . $port])) {
91             return $this->connectionCache[$host . ':' . $port];
92         }
93
94         $ldapConnection = new LdapConnection($host, $port);
95
96         if (!$ldapConnection) {
97             throw new LdapException(trans('errors.ldap_cannot_connect'));
98         }
99
100         // Set any required options
101         if ($config['version']) {
102             $ldapConnection->setVersion($config['version']);
103         }
104
105         // Start and verify TLS if it's enabled
106         if ($config['start_tls']) {
107             try {
108                 $tlsStarted = $ldapConnection->startTls();
109             } catch (ErrorException $exception) {
110                 $tlsStarted = false;
111             }
112
113             if (!$tlsStarted) {
114                 throw new LdapException('Could not start TLS connection');
115             }
116         }
117
118         return $ldapConnection;
119     }
120
121     /**
122      * Parse a potentially multi-value LDAP server host string and return an array of host/port detail pairs.
123      * Multiple hosts are separated with a semicolon, for example: 'ldap.example.com:8069;ldaps://ldap.example.com'
124      *
125      * @return array<array{host: string, port: int}>
126      */
127     protected function parseMultiServerString(string $serversString): array
128     {
129         $serverStringList = explode(';', $serversString);
130
131         return array_map(fn ($serverStr) => $this->parseSingleServerString($serverStr), $serverStringList);
132     }
133
134     /**
135      * Parse an LDAP server string and return the host and port for a connection.
136      * Is flexible to formats such as 'ldap.example.com:8069' or 'ldaps://ldap.example.com'.
137      *
138      * @return array{host: string, port: int}
139      */
140     protected function parseSingleServerString(string $serverString): array
141     {
142         $serverNameParts = explode(':', $serverString);
143
144         // If we have a protocol just return the full string since PHP will ignore a separate port.
145         if ($serverNameParts[0] === 'ldaps' || $serverNameParts[0] === 'ldap') {
146             return ['host' => $serverString, 'port' => 389];
147         }
148
149         // Otherwise, extract the port out
150         $hostName = $serverNameParts[0];
151         $ldapPort = (count($serverNameParts) > 1) ? intval($serverNameParts[1]) : 389;
152
153         return ['host' => $hostName, 'port' => $ldapPort];
154     }
155 }