Passed
Pull Request — master (#32)
by Tim
08:40
created

BaseFilter::initializeLdap()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 9
c 1
b 0
f 0
dl 0
loc 13
rs 9.9666
cc 1
nc 1
nop 0
1
<?php
2
3
/**
4
 * This base LDAP filter class can be extended to enable real filter classes direct access
5
 * access to the authsource ldap config and connects to the ldap server.
6
 *
7
 * @package simplesamlphp/simplesamlphp-module-ldap
8
 */
9
10
declare(strict_types=1);
11
12
namespace SimpleSAML\Module\ldap\Auth\Process;
13
14
use SimpleSAML\{Auth, Configuration, Error, Logger};
15
use SimpleSAML\Assert\Assert;
16
use SimpleSAML\Module\ldap\Connector;
17
use SimpleSAML\Module\ldap\ConnectorInterface;
18
19
abstract class BaseFilter extends Auth\ProcessingFilter
20
{
21
    // TODO: Support ldap:LDAPMulti, if possible
22
    protected static array $ldapsources = ['ldap:Ldap', 'authX509:X509userCert'];
23
24
    /**
25
     * List of attribute "alias's" linked to the real attribute
26
     * name. Used for abstraction / configuration of the LDAP
27
     * attribute names, which may change between dir service.
28
     *
29
     * @var array
30
     */
31
    protected array $attribute_map;
32
33
    /**
34
     * The base DN of the LDAP connection. Used when searching the LDAP server.
35
     *
36
     * @var array
37
     */
38
    protected array $searchBase;
39
40
    /**
41
     * The construct method will change the filter config into
42
     * a \SimpleSAML\Configuration object and store it here for
43
     * later use, if needed.
44
     *
45
     * @var \SimpleSAML\Configuration
46
     */
47
    protected Configuration $config;
48
49
    /**
50
     * Array of LDAP connection objects. Stored here to be accessed later during processing.
51
     *
52
     * @var \SimpleSAML\Module\ldap\ConnectorInterface
53
     */
54
    protected ConnectorInterface $connector;
55
56
    /**
57
     * The class "title" used in logging and exception messages.
58
     * This should be prepended to the beginning of the message.
59
     *
60
     * @var string
61
     */
62
    protected string $title = 'ldap:BaseFilter';
63
64
    /**
65
     * List of LDAP object types, used to determine the type of
66
     * object that a DN references.
67
     *
68
     * @var array
69
     */
70
    protected array $type_map;
71
72
73
    /**
74
     * Checks the authsource, if defined, for configuration values
75
     * to the LDAP server. Then sets up the LDAP connection for the
76
     * instance/object and stores everything in class members.
77
     *
78
     * @throws \SimpleSAML\Error\Exception
79
     * @param array &$config
80
     * @param mixed $reserved
81
     */
82
    public function __construct(array &$config, $reserved)
83
    {
84
        parent::__construct($config, $reserved);
85
86
        // Change the class $title to match it's true name
87
        // This way if the class is extended the proper name is used
88
        $classname = get_class($this);
89
        $classname = explode('_', $classname);
90
        $this->title = 'ldap:' . end($classname);
91
92
        // Log the construction
93
        Logger::debug(sprintf('%s : Creating and configuring the filter.', $this->title));
94
95
        // If an authsource was defined (an not empty string)...
96
        if (isset($config['authsource']) && $config['authsource'] !== '') {
97
            $authconfig = $this->parseAuthSourceConfig($config['authsource']);
98
99
            // Merge the authsource config with the filter config,
100
            // but have the filter config override the authsource config
101
            $config = array_merge($authconfig, $config);
102
103
            // Authsource complete
104
            Logger::debug(sprintf(
105
                '%s : Retrieved authsource [%s] configuration values: %s',
106
                $this->title,
107
                $config['authsource'],
108
                $this->varExport($authconfig)
109
            ));
110
        }
111
112
        // Convert the config array to a config class,
113
        // that way we can verify type and define defaults.
114
        // Store in the instance in-case needed later, by a child class.
115
        $this->config = Configuration::loadFromArray($config, 'ldap:AuthProcess');
116
117
        // Initialize the Ldap-object
118
        $this->connector = $this->resolveConnector();
119
120
        // Set all the filter values, setting defaults if needed
121
        $this->searchBase = $this->config->getOptionalArray('search.base', []);
122
123
        // Log the member values retrieved above
124
        Logger::debug(sprintf(
125
            '%s : Configuration values retrieved; BaseDN: %s',
126
            $this->title,
127
            $this->varExport($this->searchBase)
128
        ));
129
130
        // Setup the attribute map which will be used to search LDAP
131
        $this->attribute_map = [
132
            'dn'       => $this->config->getOptionalString('attribute.dn', 'distinguishedName'),
133
            'groups'   => $this->config->getOptionalString('attribute.groups', 'groups'),
134
            'member'   => $this->config->getOptionalString('attribute.member', 'member'),
135
            'memberOf' => $this->config->getOptionalString('attribute.memberOf', 'memberOf'),
136
            'name'     => $this->config->getOptionalString('attribute.groupname', 'name'),
137
            'return'   => $this->config->getOptionalString('attribute.return', 'distinguishedName'),
138
            'type'     => $this->config->getOptionalString('attribute.type', 'objectClass'),
139
            'username' => $this->config->getOptionalString('attribute.username', 'sAMAccountName')
140
        ];
141
142
        // Log the attribute map
143
        Logger::debug(sprintf(
144
            '%s : Attribute map created: $s',
145
            $this->title,
146
            $this->varExport($this->attribute_map)
147
        ));
148
149
        // Setup the object type map which is used to determine a DNs' type
150
        $this->type_map = [
151
            'group' => $this->config->getOptionalString('type.group', 'group'),
152
            'user'  => $this->config->getOptionalString('type.user', 'user')
153
        ];
154
155
        // Log the type map
156
        Logger::debug(sprintf(
157
            '%s : Type map created: %s',
158
            $this->title,
159
            $this->varExport($this->type_map)
160
        ));
161
    }
162
163
164
    /**
165
     * Parse authsource config
166
     *
167
     * @param string $as The name of the authsource
168
     */
169
    private function parseAuthSourceConfig(string $as): array
170
    {
171
        // Log the authsource request
172
        Logger::debug(sprintf(
173
            '%s : Attempting to get configuration values from authsource [%s]',
174
            $this->title,
175
            $as
176
        ));
177
178
        // Get the authsources file, which should contain the config
179
        $authsources = Configuration::getConfig('authsources.php');
180
181
        // Verify that the authsource config exists
182
        if (!$authsources->hasValue($as)) {
183
            throw new Error\Exception(sprintf(
184
                '%s : Authsource [%s] defined in filter parameters not found in authsources.php',
185
                $this->title,
186
                $as
187
            ));
188
        }
189
190
        // Get just the specified authsource config values
191
        $authsource = $authsources->getArray($as);
192
193
        // Make sure it is an ldap source
194
        if (isset($authsource[0]) && !in_array($authsource[0], self::$ldapsources)) {
195
            throw new Error\Exception(sprintf(
196
                '%s : Authsource [%s] specified in filter parameters is not an ldap:LDAP type',
197
                $this->title,
198
                $as
199
            ));
200
        }
201
202
        // Build the authsource config
203
        $authconfig = [];
204
        if (isset($authsource['connection_string'])) {
205
            $authconfig['connection_string'] = $authsource['connection_string'];
206
        }
207
        if (isset($authsource['encryption'])) {
208
            $authconfig['encryption'] = $authsource['encryption'];
209
        }
210
        if (isset($authsource['version'])) {
211
            $authconfig['version'] = $authsource['version'];
212
        }
213
        if (isset($authsource['timeout'])) {
214
            $authconfig['timeout'] = $authsource['timeout'];
215
        }
216
        if (isset($authsource['debug'])) {
217
            $authconfig['debug']      = $authsource['debug'];
218
        }
219
        if (isset($authsource['referrals'])) {
220
            $authconfig['referrals']  = $authsource['referrals'];
221
        }
222
223
        // only set when search.enabled = true
224
        if (isset($authsource['search.enable']) && ($authsource['search.enable'] === true)) {
225
            if (isset($authsource['search.base'])) {
226
                $authconfig['search.base'] = $authsource['search.base'];
227
            }
228
            if (isset($authsource['search.scope'])) {
229
                $authconfig['search.scope'] = $authsource['search.scope'];
230
            }
231
            if (isset($authsource['search.username'])) {
232
                $authconfig['search.username']   = $authsource['search.username'];
233
            }
234
            if (isset($authsource['search.password'])) {
235
                $authconfig['search.password']   = $authsource['search.password'];
236
            }
237
238
            // Only set the username attribute if the authsource specifies one attribute
239
            if (
240
                isset($authsource['search.attributes'])
241
                && is_array($authsource['search.attributes'])
242
                && count($authsource['search.attributes']) == 1
243
            ) {
244
                $authconfig['attribute.username'] = reset($authsource['search.attributes']);
245
            }
246
        }
247
248
        // only set when priv.read = true
249
        if (isset($authsource['priv.read']) && $authsource['priv.read']) {
250
            if (isset($authsource['priv.username'])) {
251
                $authconfig['priv.username'] = $authsource['priv.username'];
252
            }
253
            if (isset($authsource['priv.password'])) {
254
                $authconfig['priv.password'] = $authsource['priv.password'];
255
            }
256
        }
257
258
        return $authconfig;
259
    }
260
261
262
    /**
263
     * Resolve the connector
264
     *
265
     * @return \SimpleSAML\Module\ldap\ConnectorInterface
266
     * @throws \Exception
267
     */
268
    protected function resolveConnector(): ConnectorInterface
269
    {
270
        if (!empty($this->connector)) {
271
            return $this->connector;
272
        }
273
274
        $encryption = $this->config->getString('encryption', 'ssl');
0 ignored issues
show
Unused Code introduced by
The call to SimpleSAML\Configuration::getString() has too many arguments starting with 'ssl'. ( Ignorable by Annotation )

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

274
        /** @scrutinizer ignore-call */ 
275
        $encryption = $this->config->getString('encryption', 'ssl');

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
275
        Assert::oneOf($encryption, ['none', 'ssl', 'tls']);
276
277
        $version = $this->config->getInteger('version', 3);
0 ignored issues
show
Unused Code introduced by
The call to SimpleSAML\Configuration::getInteger() has too many arguments starting with 3. ( Ignorable by Annotation )

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

277
        /** @scrutinizer ignore-call */ 
278
        $version = $this->config->getInteger('version', 3);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
278
        Assert::positiveInteger($version);
279
280
        $class = $this->config->getString('connector', Connector\Ldap::class);
281
        Assert::classExists($class);
282
283
        return $this->connector = new $class(
284
            $this->config->getString('connection_string'),
285
            $this->config->getOptionalString('encryption', 'ssl'),
286
            $this->config->getOptionalInteger('version', 3),
287
            $this->config->getOptionalString('extension', 'ext_ldap'),
288
            $this->config->getOptionalBoolean('debug', false),
289
            [
290
                'network_timeout' => $this->config->getOptionalInteger('timeout', 3),
291
                'referrals' => $this->config->getOptionalBoolean('referrals', false),
292
            ]
293
        );
294
    }
295
296
297
    /**
298
     * Local utility function to get details about a variable,
299
     * basically converting it to a string to be used in a log
300
     * message. The var_export() function returns several lines
301
     * so this will remove the new lines and trim each line.
302
     *
303
     * @param mixed $value
304
     * @return string
305
     */
306
    protected function varExport($value): string
307
    {
308
        if (is_array($value)) {
309
            // remove sensitive data
310
            foreach ($value as $key => &$val) {
311
                if ($key === 'search.password') {
312
                    $val = empty($val) ? '' : '********';
313
                }
314
            }
315
            unset($val);
316
        }
317
318
        $export = var_export($value, true);
319
        $lines = explode("\n", $export);
320
        foreach ($lines as &$line) {
321
            $line = trim($line);
322
        }
323
        return implode(' ', $lines);
324
    }
325
}
326