Passed
Pull Request — master (#28)
by Tim
02:21
created

Ldap   A

Complexity

Total Complexity 18

Size/Duplication

Total Lines 213
Duplicated Lines 0 %

Importance

Changes 7
Bugs 1 Features 0
Metric Value
wmc 18
eloc 98
c 7
b 1
f 0
dl 0
loc 213
rs 10

4 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
B search() 0 49 7
A bind() 0 12 3
B login() 0 93 7
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\Module\ldap\Auth\Source;
6
7
use SimpleSAML\Assert\Assert;
8
use SimpleSAML\Configuration;
9
use SimpleSAML\Error;
10
use SimpleSAML\Logger;
11
use SimpleSAML\Module\core\Auth\UserPassBase;
12
use Symfony\Component\Ldap\Entry;
13
use Symfony\Component\Ldap\Exception\ConnectionException;
14
use Symfony\Component\Ldap\Ldap as LdapObject;
15
use Symfony\Component\Ldap\Adapter\ExtLdap\Query;
16
17
use function array_fill_keys;
18
use function array_keys;
19
use function array_map;
20
use function array_pop;
21
use function array_values;
22
use function count;
23
use function explode;
24
use function is_array;
25
use function sprintf;
26
use function strval;
27
use function str_replace;
28
use function var_export;
29
30
/**
31
 * LDAP authentication source.
32
 *
33
 * See the ldap-entry in config-templates/authsources.php for information about
34
 * configuration of this authentication source.
35
 *
36
 * @package simplesamlphp/simplesamlphp-module-ldap
37
 */
38
39
class Ldap extends UserPassBase
40
{
41
    /**
42
     * An LDAP configuration object.
43
     */
44
    private Configuration $ldapConfig;
45
46
47
    /**
48
     * Constructor for this authentication source.
49
     *
50
     * @param array $info  Information about this authentication source.
51
     * @param array $config  Configuration.
52
     */
53
    public function __construct(array $info, array $config)
54
    {
55
        // Call the parent constructor first, as required by the interface
56
        parent::__construct($info, $config);
57
58
        $this->ldapConfig = Configuration::loadFromArray(
59
            $config,
60
            'authsources[' . var_export($this->authId, true) . ']'
61
        );
62
    }
63
64
65
    /**
66
     * Attempt to log in using the given username and password.
67
     *
68
     * @param string $username  The username the user wrote.
69
     * @param string $password  The password the user wrote.
70
     * @return array  Associative array with the users attributes.
71
     */
72
    protected function login(string $username, string $password): array
73
    {
74
        $encryption = $this->ldapConfig->getString('encryption', 'ssl');
75
        Assert::oneOf($encryption, ['none', 'ssl', 'tls']);
76
77
        $version = $this->ldapConfig->getInteger('version', 3);
78
        Assert::positiveInteger($version);
79
80
        $ldapServers = [];
81
        foreach (explode(' ', $this->ldapConfig->getString('connection_string')) as $connection_string) {
82
            Assert::regex($connection_string, '#^ldap[s]?:\/\/#');
83
84
            $ldapServers[] = LdapObject::create(
85
                $this->ldapConfig->getString('extension', 'ext_ldap'),
86
                [
87
                    'connection_string' => $connection_string,
88
                    'encryption' => 'ssl',
89
                    'version' => $version,
90
                ]
91
            );
92
        }
93
94
        $searchScope = $this->ldapConfig->getString('search.scope', Query::SCOPE_SUB);
95
        Assert::oneOf($searchScope, [Query::SCOPE_BASE, Query::SCOPE_ONE, Query::SCOPE_SUB]);
96
97
        $referrals = $this->ldapConfig->getValue('referrals', Query::DEREF_NEVER);
98
        Assert::oneOf($referrals, [Query::DEREF_ALWAYS, Query::DEREF_NEVER, Query::DEREF_FINDING, Query::DEREF_SEARCHING]);
99
100
        $timeout = $this->ldapConfig->getString('timeout', 3);
101
        $searchBase = $this->ldapConfig->getArray('search.base');
102
        $options = [
103
            'scope' => $searchScope,
104
            'timeout' => $timeout,
105
            'deref' => $referrals,
106
        ];
107
108
        $searchEnable = $this->ldapConfig->getBoolean('search.enable', false);
109
        if ($searchEnable === false) {
110
            $dnPattern = $this->ldapConfig->getString('dnpattern');
111
            $dn = str_replace('%username%', $username, $dnPattern);
112
        } else {
113
            $searchUsername = $this->ldapConfig->getString('search.username');
114
            Assert::notWhitespaceOnly($searchUsername);
115
116
            $searchPassword = $this->ldapConfig->getString('search.password', null);
117
            Assert::nullOrnotWhitespaceOnly($searchPassword);
118
119
            $searchAttributes = $this->ldapConfig->getArray('search.attributes');
120
            $searchFilter = $this->ldapConfig->getString('search.filter', null);
121
122
            $ldap = $this->bind($ldapServers, $searchUsername, $searchPassword);
123
124
            $filter = '';
125
            foreach ($searchAttributes as $attr) {
126
                $filter .= '(' . $attr . '=' . $username . ')';
127
            }
128
            $filter = '(|' . $filter . ')';
129
130
            // Append LDAP filters if defined
131
            if ($searchFilter !== null) {
132
                $filter = "(&" . $filter . $searchFilter . ")";
133
            }
134
135
            /** @psalm-var \Symfony\Component\Ldap\Entry $entry */
136
            $entry = $this->search($ldap, $searchBase, $filter, $options, false);
137
            $dn = $entry->getDn();
138
        }
139
140
        $ldap = $this->bind($ldapServers, $dn, $password);
141
        $filter = sprintf('(distinguishedName=%s)', $dn);
142
143
        /** @psalm-var \Symfony\Component\Ldap\Entry $entry */
144
        $entry = $this->search($ldap, $searchBase, $filter, $options, false);
145
146
        $attributes = $this->ldapConfig->getArray('attributes', []);
147
        if ($attributes === ['*']) {
148
            $result = $entry->getAttributes();
149
        } else {
150
            $result = array_intersect_key(
151
                $entry->getAttributes(),
152
                array_fill_keys(array_values($attributes), null)
153
            );
154
        }
155
156
        $binaries = array_intersect(
157
            array_keys($result),
158
            $this->ldapConfig->getArray('attributes.binary', []),
159
        );
160
        foreach ($binaries as $binary) {
161
            $result[$binary] = array_map('base64_encode', $result[$binary]);
162
        }
163
164
        return $result;
165
    }
166
167
168
    /**
169
     * Bind to an LDAP-server
170
     *
171
     * @param \Symfony\Component\Ldap\Ldap[] $ldapServers
172
     * @param string $username
173
     * @param string|null $password  Null for passwordless logon
174
     * @throws \SimpleSAML\Error\Exception if none of the LDAP-servers could be contacted
175
     */
176
    private function bind(array $ldapServers, string $username, ?string $password)
177
    {
178
        foreach ($ldapServers as $ldap) {
179
            try {
180
                $ldap->bind($username, strval($password));
181
                return $ldap;
182
            } catch (ConnectionException $e) {
183
                // Try next server
184
            }
185
        }
186
187
        throw new Error\Exception("Unable to bind to any of the configured LDAP servers.");
188
    }
189
190
191
    /**
192
     * Search the LDAP-directory for a specific DN
193
     *
194
     * @param \Symfony\Component\Ldap\Ldap $ldap
195
     * @param array $searchBase
196
     * @param string $filter
197
     * @param array $options
198
     * @param boolean $allowMissing
199
     * @return \Symfony\Component\Ldap\Entry|null The result of the search or null if none found
200
     * @throws \SimpleSAML\Error\Exception if more than one entry was found
201
     * @throws \SimpleSAML\Error\Exception if the object cannot be found using the given search base and filter
202
     */
203
    private function search(
204
        LdapObject $ldap,
205
        array $searchBase,
206
        string $filter,
207
        array $options,
208
        bool $allowMissing
209
    ): ?Entry {
210
        $entry = null;
211
212
        foreach ($searchBase as $base) {
213
            $query = $ldap->query($base, $filter, $options);
214
            $result = $query->execute();
215
            $result = is_array($result) ? $result : $result->toArray();
216
217
            if (count($result) > 1) {
218
                throw new Error\Exception(
219
                    sprintf(
220
                        "Library - LDAP search(): Found %d entries searching base '%s' for '%s'",
221
                        count($result),
222
                        $base,
223
                        $filter,
224
                    )
225
                );
226
            } elseif (count($result) === 1) {
227
                $entry = array_pop($result);
228
                break;
229
            } else {
230
                Logger::debug(
231
                    sprintf(
232
                        "Library - LDAP search(): Found no entries searching base '%s' for '%s'",
233
                        count($result),
234
                        $base,
235
                        $filter,
236
                    )
237
                );
238
            }
239
        }
240
241
        if ($entry === null && $allowMissing === false) {
242
            throw new Error\Exception(
243
                sprintf(
244
                    "Object not found using search base [%s] and filter '%s'",
245
                    implode(', ', $searchBase),
246
                    $filter
247
                )
248
            );
249
        }
250
251
        return $entry;
252
    }
253
}
254