Passed
Pull Request — master (#37)
by Tim
03:03
created

Ldap::getAttributes()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 42
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 27
c 0
b 0
f 0
dl 0
loc 42
rs 9.488
cc 4
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\ConnectorFactory;
12
use SimpleSAML\Module\ldap\ConnectorInterface;
13
use Symfony\Component\Ldap\Adapter\ExtLdap\Query;
14
use Symfony\Component\Ldap\Entry;
15
16
use function array_fill_keys;
17
use function array_keys;
18
use function array_map;
19
use function array_values;
20
use function sprintf;
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
    /**
36
     * @var \SimpleSAML\Module\ldap\ConnectorInterface
37
     */
38
    protected ConnectorInterface $connector;
39
40
    /**
41
     * An LDAP configuration object.
42
     */
43
    protected Configuration $ldapConfig;
44
45
46
    /**
47
     * Constructor for this authentication source.
48
     *
49
     * @param array $info  Information about this authentication source.
50
     * @param array $config  Configuration.
51
     */
52
    public function __construct(array $info, array $config)
53
    {
54
        // Call the parent constructor first, as required by the interface
55
        parent::__construct($info, $config);
56
57
        $this->ldapConfig = Configuration::loadFromArray(
58
            $config,
59
            'authsources[' . var_export($this->authId, true) . ']'
60
        );
61
62
        $this->connector = ConnectorFactory::fromAuthSource($this->authId);
63
    }
64
65
66
    /**
67
     * Attempt to log in using the given username and password.
68
     *
69
     * @param string $username  The username the user wrote.
70
     * @param string $password  The password the user wrote.
71
     * @return array  Associative array with the users attributes.
72
     */
73
    protected function login(string $username, string $password): array
74
    {
75
        $searchScope = $this->ldapConfig->getOptionalString('search.scope', Query::SCOPE_SUB);
76
        Assert::oneOf($searchScope, [Query::SCOPE_BASE, Query::SCOPE_ONE, Query::SCOPE_SUB]);
77
78
        $timeout = $this->ldapConfig->getOptionalInteger('timeout', 3);
79
        Assert::natural($timeout);
80
81
        $searchBase = $this->ldapConfig->getArray('search.base');
82
        $options = [
83
            'scope' => $searchScope,
84
            'timeout' => $timeout,
85
        ];
86
87
        $searchEnable = $this->ldapConfig->getOptionalBoolean('search.enable', false);
88
        if ($searchEnable === false) {
89
            $dnPattern = $this->ldapConfig->getString('dnpattern');
90
            $dn = str_replace('%username%', $username, $dnPattern);
91
        } else {
92
            $searchUsername = $this->ldapConfig->getString('search.username');
93
            Assert::notWhitespaceOnly($searchUsername);
94
95
            $searchPassword = $this->ldapConfig->getOptionalString('search.password', null);
96
            Assert::nullOrnotWhitespaceOnly($searchPassword);
97
98
            try {
99
                $this->connector->bind($searchUsername, $searchPassword);
100
            } catch (Error\Error $e) {
101
                throw new Error\Exception("Unable to bind using the configured search.username and search.password.");
102
            }
103
104
            $filter = $this->buildSearchFilter($username);
105
106
            try {
107
                /** @psalm-var \Symfony\Component\Ldap\Entry $entry */
108
                $entry = $this->connector->search($searchBase, $filter, $options, false);
109
                $dn = $entry->getDn();
110
            } catch (Error\Exception $e) {
111
                throw new Error\Error('WRONGUSERPASS');
112
            }
113
        }
114
115
        $this->connector->bind($dn, $password);
116
117
        $options['scope'] = Query::SCOPE_BASE;
118
        $filter = '(objectClass=*)';
119
120
        /** @psalm-var \Symfony\Component\Ldap\Entry $entry */
121
        $entry = $this->connector->search([$dn], $filter, $options, false);
122
123
        return $this->processAttributes($entry);
0 ignored issues
show
Bug introduced by
It seems like $entry can also be of type null; however, parameter $entry of SimpleSAML\Module\ldap\A...ap::processAttributes() does only seem to accept Symfony\Component\Ldap\Entry, 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

123
        return $this->processAttributes(/** @scrutinizer ignore-type */ $entry);
Loading history...
124
    }
125
126
127
    /**
128
     * Attempt to find a user's attributes given its username.
129
     *
130
     * @param string $username  The username who's attributes we want.
131
     * @return array  Associative array with the users attributes.
132
     */
133
    public function getAttributes(string $username): array
134
    {
135
        $searchUsername = $this->ldapConfig->getString('search.username');
136
        Assert::notWhitespaceOnly($searchUsername);
137
138
        $searchPassword = $this->ldapConfig->getOptionalString('search.password', null);
139
        Assert::nullOrnotWhitespaceOnly($searchPassword);
140
141
        try {
142
            $this->connector->bind($searchUsername, $searchPassword);
143
        } catch (Error\Error $e) {
144
            throw new Error\Exception("Unable to bind using the configured search.username and search.password.");
145
        }
146
147
        $searchEnable = $this->ldapConfig->getOptionalBoolean('search.enable', false);
148
        if ($searchEnable === false) {
149
            $dnPattern = $this->ldapConfig->getString('dnpattern');
150
            $filter = '(' . str_replace('%username%', $username, $dnPattern) . ')';
151
        } else {
152
            $filter = $this->buildSearchFilter($username);
153
        }
154
155
        $searchScope = $this->ldapConfig->getOptionalString('search.scope', Query::SCOPE_SUB);
156
        Assert::oneOf($searchScope, [Query::SCOPE_BASE, Query::SCOPE_ONE, Query::SCOPE_SUB]);
157
158
        $timeout = $this->ldapConfig->getOptionalInteger('timeout', 3);
159
        Assert::natural($timeout);
160
161
        $searchBase = $this->ldapConfig->getArray('search.base');
162
        $options = [
163
            'scope' => $searchScope,
164
            'timeout' => $timeout,
165
        ];
166
167
        try {
168
            /** @psalm-var \Symfony\Component\Ldap\Entry $entry */
169
            $entry = $this->connector->search($searchBase, $filter, $options, false);
170
        } catch (Error\Exception $e) {
171
            throw new Error\Error('WRONGUSERPASS');
172
        }
173
174
        return $this->processAttributes($entry);
0 ignored issues
show
Bug introduced by
It seems like $entry can also be of type null; however, parameter $entry of SimpleSAML\Module\ldap\A...ap::processAttributes() does only seem to accept Symfony\Component\Ldap\Entry, 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

174
        return $this->processAttributes(/** @scrutinizer ignore-type */ $entry);
Loading history...
175
    }
176
177
178
    /**
179
     * @param \Symfony\Component\Ldap\Entry $entry
180
     * @return array
181
     */
182
    private function processAttributes(Entry $entry): array
183
    {
184
        $attributes = $this->ldapConfig->getOptionalValue('attributes', []);
185
        if ($attributes === null) {
186
            $result = $entry->getAttributes();
187
        } else {
188
            Assert::isArray($attributes);
189
            $result = array_intersect_key(
190
                $entry->getAttributes(),
191
                array_fill_keys(array_values($attributes), null)
192
            );
193
        }
194
195
        $binaries = array_intersect(
196
            array_keys($result),
197
            $this->ldapConfig->getOptionalArray('attributes.binary', []),
198
        );
199
        foreach ($binaries as $binary) {
200
            $result[$binary] = array_map('base64_encode', $result[$binary]);
201
        }
202
203
        return $result;
204
    }
205
206
207
    /**
208
     * @param string $username
209
     * @return string
210
     */
211
    private function buildSearchFilter(string $username): string
212
    {
213
        $searchAttributes = $this->ldapConfig->getArray('search.attributes');
214
        /** @psalm-var string|null $searchFilter */
215
        $searchFilter = $this->ldapConfig->getOptionalString('search.filter', null);
216
217
        $filter = '';
218
        foreach ($searchAttributes as $attr) {
219
            $filter .= '(' . $attr . '=' . $username . ')';
220
        }
221
        $filter = '(|' . $filter . ')';
222
223
        // Append LDAP filters if defined
224
        if ($searchFilter !== null) {
225
            $filter = "(&" . $filter . $searchFilter . ")";
226
        }
227
228
        return $filter;
229
    }
230
}
231