Ldap::getAttributes()   A
last analyzed

Complexity

Conditions 5
Paths 5

Size

Total Lines 49
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 31
c 1
b 0
f 0
dl 0
loc 49
rs 9.1128
cc 5
nc 5
nop 1
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\Module\core\Auth\UserPassBase;
11
use SimpleSAML\Module\ldap\Connector\LdapHelpers;
12
use SimpleSAML\Module\ldap\ConnectorFactory;
13
use SimpleSAML\Module\ldap\ConnectorInterface;
14
use Symfony\Component\Ldap\Adapter\ExtLdap\Query;
15
use Symfony\Component\Ldap\Entry;
16
17
use function array_keys;
18
use function array_map;
19
use function in_array;
20
use function preg_match;
21
use function str_replace;
22
use function var_export;
23
24
/**
25
 * LDAP authentication source.
26
 *
27
 * See the ldap-entry in config-templates/authsources.php for information about
28
 * configuration of this authentication source.
29
 *
30
 * @package simplesamlphp/simplesamlphp-module-ldap
31
 */
32
33
class Ldap extends UserPassBase
34
{
35
    use LdapHelpers;
36
37
38
    /**
39
     * @var \SimpleSAML\Module\ldap\ConnectorInterface
40
     */
41
    protected ConnectorInterface $connector;
42
43
    /**
44
     * An LDAP configuration object.
45
     */
46
    protected Configuration $ldapConfig;
47
48
49
    /**
50
     * Constructor for this authentication source.
51
     *
52
     * @param array<mixed> $info  Information about this authentication source.
53
     * @param array<mixed> $config  Configuration.
54
     */
55
    public function __construct(array $info, array $config)
56
    {
57
        // Call the parent constructor first, as required by the interface
58
        parent::__construct($info, $config);
59
60
        $this->ldapConfig = Configuration::loadFromArray(
61
            $config,
62
            'authsources[' . var_export($this->authId, true) . ']',
63
        );
64
65
        $this->connector = ConnectorFactory::fromAuthSource($this->authId);
66
    }
67
68
69
    /**
70
     * Attempt to log in using SASL and the given username and password.
71
     *
72
     * @param string $username  The username the user wrote.
73
     * @param string $password  The password the user wrote.
74
     * @param array<mixed> $sasl_args  SASL options
75
     * @return array<mixed> Associative array with the users attributes.
76
     */
77
    protected function loginSasl(
78
        string $username,
79
        #[\SensitiveParameter]
80
        string $password,
81
        array $sasl_args = [],
82
    ): array {
83
        if (preg_match('/^\s*$/', $password)) {
84
            // The empty string is considered an anonymous bind to Symfony
85
            throw new Error\Error('WRONGUSERPASS');
86
        }
87
88
        $searchScope = $this->ldapConfig->getOptionalString('search.scope', Query::SCOPE_SUB);
89
        Assert::oneOf($searchScope, [Query::SCOPE_BASE, Query::SCOPE_ONE, Query::SCOPE_SUB]);
90
91
        $timeout = $this->ldapConfig->getOptionalInteger('timeout', 3);
92
        Assert::natural($timeout);
93
94
        $attributes = $this->ldapConfig->getOptionalValue(
95
            'attributes',
96
            // If specifically set to NULL return all attributes, if not set at all return nothing (safe default)
97
            in_array('attributes', $this->ldapConfig->getOptions(), true) ? ['*'] : [],
98
        );
99
100
        $searchBase = $this->ldapConfig->getArray('search.base');
101
102
        $options = [
103
            'scope' => $searchScope,
104
            'timeout' => $timeout,
105
            'filter' => $attributes,
106
        ];
107
108
        $searchEnable = $this->ldapConfig->getOptionalBoolean('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->getOptionalString('search.username', null);
114
            Assert::nullOrNotWhitespaceOnly($searchUsername);
115
116
            $searchPassword = $this->ldapConfig->getOptionalString('search.password', null);
117
            Assert::nullOrNotWhitespaceOnly($searchPassword);
118
119
            try {
120
                $this->connector->bind($searchUsername, $searchPassword);
121
            } catch (Error\Error $e) {
122
                throw new Error\Exception("Unable to bind using the configured search.username and search.password.");
123
            }
124
125
            $filter = $this->buildSearchFilter($username);
126
127
            try {
128
                $entry = /** @scrutinizer-ignore-type */$this->connector->search($searchBase, $filter, $options, false);
129
                $dn = $entry->getDn();
130
            } catch (Error\Exception $e) {
131
                throw new Error\Error('WRONGUSERPASS');
132
            }
133
        }
134
135
        /* Verify the credentials */
136
        if (!empty($sasl_args)) {
137
            $this->connector->saslBind(
138
                $dn,
139
                $password,
140
                $sasl_args['mech'],
141
                $sasl_args['realm'],
142
                $sasl_args['authc_id'],
143
                $sasl_args['authz_id'],
144
                $sasl_args['props'],
145
            );
146
            $dn = $this->connector->whoami();
147
        } else {
148
            $this->connector->bind($dn, $password);
149
        }
150
151
        /* If the credentials were correct, rebind using a privileged account to read attributes */
152
        $readUsername = $this->ldapConfig->getOptionalString('priv.username', null);
153
        $readPassword = $this->ldapConfig->getOptionalString('priv.password', null);
154
        if ($readUsername !== null) {
155
            $this->connector->bind($readUsername, $readPassword);
156
        }
157
158
        $options['scope'] = Query::SCOPE_BASE;
159
        $filter = '(objectClass=*)';
160
161
        $entry = $this->connector->search([$dn], $filter, $options, false);
162
163
        return $this->processAttributes(/** @scrutinizer-ignore-type */$entry);
164
    }
165
166
    /**
167
     * Attempt to log in using the given username and password.
168
     *
169
     * @param string $username  The username the user wrote.
170
     * @param string $password  The password the user wrote.
171
     * @return array<mixed> Associative array with the users attributes.
172
     */
173
    protected function login(string $username, #[\SensitiveParameter]string $password): array
174
    {
175
        return $this->loginSasl($username, $password);
176
    }
177
178
179
    /**
180
     * Attempt to find a user's attributes given its username.
181
     *
182
     * @param string $username  The username who's attributes we want.
183
     * @return array<mixed> Associative array with the users attributes.
184
     */
185
    public function getAttributes(string $username): array
186
    {
187
        $searchUsername = $this->ldapConfig->getOptionalString('search.username', null);
188
        Assert::nullOrNotWhitespaceOnly($searchUsername);
189
190
        $searchPassword = $this->ldapConfig->getOptionalString('search.password', null);
191
        Assert::nullOrNotWhitespaceOnly($searchPassword);
192
193
        try {
194
            $this->connector->bind($searchUsername, $searchPassword);
195
        } catch (Error\Error $e) {
196
            throw new Error\Exception("Unable to bind using the configured search.username and search.password.");
197
        }
198
199
        $searchEnable = $this->ldapConfig->getOptionalBoolean('search.enable', false);
200
        if ($searchEnable === false) {
201
            $dnPattern = $this->ldapConfig->getString('dnpattern');
202
            $filter = '(' . str_replace('%username%', $this->escapeFilterValue($username), $dnPattern) . ')';
203
        } else {
204
            $filter = $this->buildSearchFilter($username);
205
        }
206
207
        $searchScope = $this->ldapConfig->getOptionalString('search.scope', Query::SCOPE_SUB);
208
        Assert::oneOf($searchScope, [Query::SCOPE_BASE, Query::SCOPE_ONE, Query::SCOPE_SUB]);
209
210
        $timeout = $this->ldapConfig->getOptionalInteger('timeout', 3);
211
        Assert::natural($timeout);
212
213
        $attributes = $this->ldapConfig->getOptionalValue(
214
            'attributes',
215
            // If specifically set to NULL return all attributes, if not set at all return nothing (safe default)
216
            in_array('attributes', $this->ldapConfig->getOptions(), true) ? ['*'] : [],
217
        );
218
219
        $searchBase = $this->ldapConfig->getArray('search.base');
220
        $options = [
221
            'scope' => $searchScope,
222
            'timeout' => $timeout,
223
            'filter' => $attributes,
224
        ];
225
226
        try {
227
            /** @var \Symfony\Component\Ldap\Entry $entry */
228
            $entry = $this->connector->search($searchBase, $filter, $options, false);
229
        } catch (Error\Exception $e) {
230
            throw new Error\Error('WRONGUSERPASS');
231
        }
232
233
        return $this->processAttributes($entry);
234
    }
235
236
237
    /**
238
     * @param \Symfony\Component\Ldap\Entry $entry
239
     * @return array<mixed>
240
     */
241
    private function processAttributes(Entry $entry): array
242
    {
243
        $result = $entry->getAttributes();
244
245
        $binaries = array_intersect(
246
            array_keys($result),
247
            $this->ldapConfig->getOptionalArray('attributes.binary', []),
0 ignored issues
show
Bug introduced by
It seems like $this->ldapConfig->getOp...butes.binary', array()) can also be of type null; however, parameter $arrays of array_intersect() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

247
            /** @scrutinizer ignore-type */ $this->ldapConfig->getOptionalArray('attributes.binary', []),
Loading history...
248
        );
249
250
        foreach ($binaries as $binary) {
251
            $result[$binary] = array_map('base64_encode', $result[$binary]);
252
        }
253
254
        return $result;
255
    }
256
257
258
    /**
259
     * @param string $username
260
     * @return string
261
     */
262
    private function buildSearchFilter(string $username): string
263
    {
264
        $searchAttributes = $this->ldapConfig->getArray('search.attributes');
265
        /** @psalm-var string|null $searchFilter */
266
        $searchFilter = $this->ldapConfig->getOptionalString('search.filter', null);
267
268
        $filter = '';
269
        foreach ($searchAttributes as $attr) {
270
            $filter .= '(' . $attr . '=' . $this->escapeFilterValue($username) . ')';
0 ignored issues
show
Bug introduced by
Are you sure $this->escapeFilterValue($username) of type string|string[] can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

270
            $filter .= '(' . $attr . '=' . /** @scrutinizer ignore-type */ $this->escapeFilterValue($username) . ')';
Loading history...
271
        }
272
        $filter = '(|' . $filter . ')';
273
274
        // Append LDAP filters if defined
275
        if ($searchFilter !== null) {
276
            $filter = "(&" . $filter . $searchFilter . ")";
277
        }
278
279
        return $filter;
280
    }
281
282
283
    /**
284
     * @return \SimpleSAML\Module\ldap\ConnectorInterface
285
     */
286
    public function getConnector(): ConnectorInterface
287
    {
288
        return $this->connector;
289
    }
290
}
291