Passed
Branch master (16dc92)
by Tim
05:35 queued 01:28
created

Ldap::getConnector()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 0
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\ConnectorFactory;
12
use SimpleSAML\Module\ldap\ConnectorInterface;
13
use SimpleSAML\Module\ldap\Connector\LdapHelpers;
14
use Symfony\Component\Ldap\Adapter\ExtLdap\Query;
15
use Symfony\Component\Ldap\Entry;
16
17
use function array_fill_keys;
18
use function array_keys;
19
use function array_map;
20
use function array_values;
21
use function in_array;
22
use function sprintf;
23
use function str_replace;
24
use function var_export;
25
26
/**
27
 * LDAP authentication source.
28
 *
29
 * See the ldap-entry in config-templates/authsources.php for information about
30
 * configuration of this authentication source.
31
 *
32
 * @package simplesamlphp/simplesamlphp-module-ldap
33
 */
34
35
class Ldap extends UserPassBase
36
{
37
    use LdapHelpers;
38
39
40
    /**
41
     * @var \SimpleSAML\Module\ldap\ConnectorInterface
42
     */
43
    protected ConnectorInterface $connector;
44
45
    /**
46
     * An LDAP configuration object.
47
     */
48
    protected Configuration $ldapConfig;
49
50
51
    /**
52
     * Constructor for this authentication source.
53
     *
54
     * @param array $info  Information about this authentication source.
55
     * @param array $config  Configuration.
56
     */
57
    public function __construct(array $info, array $config)
58
    {
59
        // Call the parent constructor first, as required by the interface
60
        parent::__construct($info, $config);
61
62
        $this->ldapConfig = Configuration::loadFromArray(
63
            $config,
64
            'authsources[' . var_export($this->authId, true) . ']'
65
        );
66
67
        $this->connector = ConnectorFactory::fromAuthSource($this->authId);
68
    }
69
70
71
    /**
72
     * Attempt to log in using the given username and password.
73
     *
74
     * @param string $username  The username the user wrote.
75
     * @param string $password  The password the user wrote.
76
     * @return array  Associative array with the users attributes.
77
     */
78
    protected function login(string $username, string $password): array
79
    {
80
        $searchScope = $this->ldapConfig->getOptionalString('search.scope', Query::SCOPE_SUB);
81
        Assert::oneOf($searchScope, [Query::SCOPE_BASE, Query::SCOPE_ONE, Query::SCOPE_SUB]);
82
83
        $timeout = $this->ldapConfig->getOptionalInteger('timeout', 3);
84
        Assert::natural($timeout);
85
86
        $searchBase = $this->ldapConfig->getArray('search.base');
87
        $options = [
88
            'scope' => $searchScope,
89
            'timeout' => $timeout,
90
        ];
91
92
        $searchEnable = $this->ldapConfig->getOptionalBoolean('search.enable', false);
93
        if ($searchEnable === false) {
94
            $dnPattern = $this->ldapConfig->getString('dnpattern');
95
            $dn = str_replace('%username%', $username, $dnPattern);
96
        } else {
97
            $searchUsername = $this->ldapConfig->getOptionalString('search.username', null);
98
            Assert::nullOrNotWhitespaceOnly($searchUsername);
99
100
            $searchPassword = $this->ldapConfig->getOptionalString('search.password', null);
101
            Assert::nullOrnotWhitespaceOnly($searchPassword);
102
103
            try {
104
                $this->connector->bind($searchUsername, $searchPassword);
105
            } catch (Error\Error $e) {
106
                throw new Error\Exception("Unable to bind using the configured search.username and search.password.");
107
            }
108
109
            $filter = $this->buildSearchFilter($username);
110
111
            try {
112
                $entry = /** @scrutinizer-ignore-type */$this->connector->search($searchBase, $filter, $options, false);
113
                $dn = $entry->getDn();
114
            } catch (Error\Exception $e) {
115
                throw new Error\Error('WRONGUSERPASS');
116
            }
117
        }
118
119
        $this->connector->bind($dn, $password);
120
121
        $options['scope'] = Query::SCOPE_BASE;
122
        $filter = '(objectClass=*)';
123
124
        $entry = $this->connector->search([$dn], $filter, $options, false);
125
126
        return $this->processAttributes(/** @scrutinizer-ignore-type */$entry);
127
    }
128
129
130
    /**
131
     * Attempt to find a user's attributes given its username.
132
     *
133
     * @param string $username  The username who's attributes we want.
134
     * @return array  Associative array with the users attributes.
135
     */
136
    public function getAttributes(string $username): array
137
    {
138
        $searchUsername = $this->ldapConfig->getOptionalString('search.username', null);
139
        Assert::nullOrNotWhitespaceOnly($searchUsername);
140
141
        $searchPassword = $this->ldapConfig->getOptionalString('search.password', null);
142
        Assert::nullOrnotWhitespaceOnly($searchPassword);
143
144
        try {
145
            $this->connector->bind($searchUsername, $searchPassword);
146
        } catch (Error\Error $e) {
147
            throw new Error\Exception("Unable to bind using the configured search.username and search.password.");
148
        }
149
150
        $searchEnable = $this->ldapConfig->getOptionalBoolean('search.enable', false);
151
        if ($searchEnable === false) {
152
            $dnPattern = $this->ldapConfig->getString('dnpattern');
153
            $filter = '(' . str_replace('%username%', $this->escapeFilterValue($username), $dnPattern) . ')';
154
        } else {
155
            $filter = $this->buildSearchFilter($username);
156
        }
157
158
        $searchScope = $this->ldapConfig->getOptionalString('search.scope', Query::SCOPE_SUB);
159
        Assert::oneOf($searchScope, [Query::SCOPE_BASE, Query::SCOPE_ONE, Query::SCOPE_SUB]);
160
161
        $timeout = $this->ldapConfig->getOptionalInteger('timeout', 3);
162
        Assert::natural($timeout);
163
164
        $searchBase = $this->ldapConfig->getArray('search.base');
165
        $options = [
166
            'scope' => $searchScope,
167
            'timeout' => $timeout,
168
        ];
169
170
        try {
171
            /** @var \Symfony\Component\Ldap\Entry $entry */
172
            $entry = $this->connector->search($searchBase, $filter, $options, false);
173
        } catch (Error\Exception $e) {
174
            throw new Error\Error('WRONGUSERPASS');
175
        }
176
177
        return $this->processAttributes($entry);
178
    }
179
180
181
    /**
182
     * @param \Symfony\Component\Ldap\Entry $entry
183
     * @return array
184
     */
185
    private function processAttributes(Entry $entry): array
186
    {
187
        $attributes = $this->ldapConfig->getOptionalValue(
188
            'attributes',
189
            // If specifically set to NULL return all attributes, if not set at all return nothing (safe default)
190
            in_array('attributes', $this->ldapConfig->getOptions(), true) ? null : [],
191
        );
192
193
        if ($attributes === null) {
194
            $result = $entry->getAttributes();
195
        } else {
196
            Assert::isArray($attributes);
197
            $result = array_intersect_key(
198
                $entry->getAttributes(),
199
                array_fill_keys(array_values($attributes), null)
200
            );
201
        }
202
203
        $binaries = array_intersect(
204
            array_keys($result),
205
            $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

205
            /** @scrutinizer ignore-type */ $this->ldapConfig->getOptionalArray('attributes.binary', []),
Loading history...
206
        );
207
        foreach ($binaries as $binary) {
208
            $result[$binary] = array_map('base64_encode', $result[$binary]);
209
        }
210
211
        return $result;
212
    }
213
214
215
    /**
216
     * @param string $username
217
     * @return string
218
     */
219
    private function buildSearchFilter(string $username): string
220
    {
221
        $searchAttributes = $this->ldapConfig->getArray('search.attributes');
222
        /** @psalm-var string|null $searchFilter */
223
        $searchFilter = $this->ldapConfig->getOptionalString('search.filter', null);
224
225
        $filter = '';
226
        foreach ($searchAttributes as $attr) {
227
            $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

227
            $filter .= '(' . $attr . '=' . /** @scrutinizer ignore-type */ $this->escapeFilterValue($username) . ')';
Loading history...
228
        }
229
        $filter = '(|' . $filter . ')';
230
231
        // Append LDAP filters if defined
232
        if ($searchFilter !== null) {
233
            $filter = "(&" . $filter . $searchFilter . ")";
234
        }
235
236
        return $filter;
237
    }
238
239
240
    /**
241
     * @return \SimpleSAML\Module\ldap\ConnectorInterface
242
     */
243
    public function getConnector(): ConnectorInterface
244
    {
245
        return $this->connector;
246
    }
247
}
248