Passed
Branch symfony (d145bc)
by Tim
02:06
created

Ldap::search()   B

Complexity

Conditions 7
Paths 12

Size

Total Lines 49
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 29
c 1
b 0
f 0
dl 0
loc 49
rs 8.5226
cc 7
nc 12
nop 5
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
113
            $filter = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $filter is dead and can be removed.
Loading history...
114
        } else {
115
            $searchUsername = $this->ldapConfig->getString('search.username');
116
            Assert::notWhitespaceOnly($searchUsername);
117
118
            $searchPassword = $this->ldapConfig->getString('search.password', null);
119
            Assert::nullOrnotWhitespaceOnly($searchPassword);
120
121
            $searchAttributes = $this->ldapConfig->getArray('search.attributes');
122
            $searchFilter = $this->ldapConfig->getString('search.filter', null);
123
124
            $ldap = $this->bind($ldapServers, $searchUsername, $searchPassword);
125
126
            $filter = '';
127
            foreach ($searchAttributes as $attr) {
128
                $filter .= '(' . $attr . '=' . $username . ')';
129
            }
130
            $filter = '(|' . $filter . ')';
131
132
            // Append LDAP filters if defined
133
            if ($searchFilter !== null) {
134
                $filter = "(&" . $filter . $searchFilter . ")";
135
            }
136
137
            /** @psalm-var \Symfony\Component\Ldap\Entry $entry */
138
            $entry = $this->search($ldap, $searchBase, $filter, $options, false);
139
            $dn = $entry->getDn();
140
        }
141
142
        $ldap = $this->bind($ldapServers, $dn, $password);
143
        $filter = sprintf('(distinguishedName=%s)', $dn);
144
145
        /** @psalm-var \Symfony\Component\Ldap\Entry $entry */
146
        $entry = $this->search($ldap, $searchBase, $filter, $options, false);
147
148
        $attributes = $this->ldapConfig->getArray('attributes', []);
149
        if ($attributes === ['*']) {
150
            $result = $entry->getAttributes();
151
        } else {
152
            $result = array_intersect_key(
153
                $entry->getAttributes(),
154
                array_fill_keys(array_values($attributes), null)
155
            );
156
        }
157
158
        $binaries = array_intersect(
159
            array_keys($result),
160
            $this->ldapConfig->getArray('attributes.binary', []),
161
        );
162
        foreach ($binaries as $binary) {
163
            $result[$binary] = array_map('base64_encode', $result[$binary]);
164
        }
165
166
        return $result;
167
    }
168
169
170
    /**
171
     * Bind to an LDAP-server
172
     *
173
     * @param \Symfony\Component\Ldap\Ldap[] $ldapServers
174
     * @param string $username
175
     * @param string|null $password  Null for passwordless logon
176
     * @throws \SimpleSAML\Error\Exception if none of the LDAP-servers could be contacted
177
     */
178
    private function bind(array $ldapServers, string $username, ?string $password)
179
    {
180
        foreach ($ldapServers as $ldap) {
181
            try {
182
                $ldap->bind($username, strval($password));
183
                return $ldap;
184
            } catch (ConnectionException $e) {
185
                // Try next server
186
            }
187
        }
188
189
        throw new Error\Exception("Unable to bind to any of the configured LDAP servers.");
190
    }
191
192
193
    /**
194
     * Search the LDAP-directory for a specific DN
195
     *
196
     * @param \Symfony\Component\Ldap\Ldap $ldap
197
     * @param array $searchBase
198
     * @param string $filter
199
     * @param array $options
200
     * @param boolean $allowMissing
201
     * @return \Symfony\Component\Ldap\Entry|null The result of the search or null if none found
202
     * @throws \SimpleSAML\Error\Exception if more than one entry was found
203
     * @throws \SimpleSAML\Error\Exception if the object cannot be found using the given search base and filter
204
     */
205
    private function search(
206
        LdapObject $ldap,
207
        array $searchBase,
208
        string $filter,
209
        array $options,
210
        bool $allowMissing
211
    ): ?Entry {
212
        $entry = null;
213
214
        foreach ($searchBase as $base) {
215
            $query = $ldap->query($base, $filter, $options);
216
            $result = $query->execute();
217
            $result = is_array($result) ? $result : $result->toArray();
218
219
            if (count($result) > 1) {
220
                throw new Error\Exception(
221
                    sprintf(
222
                        "Library - LDAP search(): Found %d entries searching base '%s' for '%s'",
223
                        count($result),
224
                        $base,
225
                        $filter,
226
                    )
227
                );
228
            } elseif (count($result) === 1) {
229
                $entry = array_pop($result);
230
                break;
231
            } else {
232
                Logger::debug(
233
                    sprintf(
234
                        "Library - LDAP search(): Found no entries searching base '%s' for '%s'",
235
                        count($result),
236
                        $base,
237
                        $filter,
238
                    )
239
                );
240
            }
241
        }
242
243
        if ($entry === null && $allowMissing === false) {
244
            throw new Error\Exception(
245
                sprintf(
246
                    "Object not found using search base [%s] and filter '%s'",
247
                    implode(', ', $searchBase),
248
                    $filter
249
                )
250
            );
251
        }
252
253
        return $entry;
254
    }
255
}
256