Passed
Pull Request — master (#3)
by Tim
02:51
created

ConsentAdmin   A

Complexity

Total Complexity 36

Size/Duplication

Total Lines 389
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 161
c 1
b 0
f 0
dl 0
loc 389
rs 9.52
wmc 36

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
A setAuthSimple() 0 3 1
A setConsent() 0 3 1
A setMetadataStorageHandler() 0 3 1
A driveProcessingChain() 0 63 4
F main() 0 226 28
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\Module\consentadmin\Controller;
6
7
use Exception;
8
use SimpleSAML\Auth;
9
use SimpleSAML\Configuration;
10
use SimpleSAML\Logger;
11
use SimpleSAML\Metadata\MetaDataStorageHandler;
12
use SimpleSAML\Module\consent\Auth\Process\Consent;
13
use SimpleSAML\Module\consent\Store;
14
use SimpleSAML\Session;
15
use SimpleSAML\XHTML\Template;
16
use Symfony\Component\HttpFoundation\Request;
17
18
/**
19
 * Controller class for the consentadmin module.
20
 *
21
 * This class serves the different views available in the module.
22
 *
23
 * @package simplesamlphp/simplesamlphp-module-consentadmin
24
 */
25
class ConsentAdmin
26
{
27
    /** @var \SimpleSAML\Configuration */
28
    protected Configuration $config;
29
30
    /** @var \SimpleSAML\Session */
31
    protected Session $session;
32
33
    /** @var \SimpleSAML\Metadata\MetaDataStorageHandler */
34
    protected MetaDataStorageHandler $metadataStorageHandler;
35
36
    /**
37
     * @var \SimpleSAML\Auth\Simple|string
38
     * @psalm-var \SimpleSAML\Auth\Simple|class-string
39
     */
40
    protected $authSimple = Auth\Simple::class;
41
42
    /**
43
     * @var \SimpleSAML\Module\consent\Auth\Process\Consent|string
44
     * @psalm-var \SimpleSAML\Module\consent\Auth\Process\Consent|class-string
45
     */
46
    protected $consent = Consent::class;
47
48
49
    /**
50
     * Controller constructor.
51
     *
52
     * It initializes the global configuration and session for the controllers implemented here.
53
     *
54
     * @param \SimpleSAML\Configuration $config The configuration to use by the controllers.
55
     * @param \SimpleSAML\Session $session The session to use by the controllers.
56
     *
57
     * @throws \Exception
58
     */
59
    public function __construct(
60
        Configuration $config,
61
        Session $session
62
    ) {
63
        $this->config = $config;
64
        $this->moduleConfig = Configuration::getConfig('module_consentAdmin.php');
0 ignored issues
show
Bug Best Practice introduced by
The property moduleConfig does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
65
        $this->metadataStorageHandler = MetaDataStorageHandler::getMetadataHandler();
66
        $this->session = $session;
67
    }
68
69
70
    /**
71
     * Inject the \SimpleSAML\Auth\Simple dependency.
72
     *
73
     * @param \SimpleSAML\Auth\Simple $authSimple
74
     */
75
    public function setAuthSimple(Auth\Simple $authSimple): void
76
    {
77
        $this->authSimple = $authSimple;
78
    }
79
80
81
    /**
82
     * Inject the \SimpleSAML\Metadata\MetaDataStorageHandler dependency.
83
     *
84
     * @param \SimpleSAML\Metadata\MetaDataStorageHandler $handler
85
     */
86
    public function setMetadataStorageHandler(MetadataStorageHandler $handler): void
87
    {
88
        $this->metadataStorageHandler = $handler;
89
    }
90
91
92
    /**
93
     * Inject the \SimpleSAML\Module\consent\Auth\Process\Consent dependency.
94
     *
95
     * @param \SimpleSAML\Module\consent\Auth\Process\Consent $consent
96
     */
97
    public function setConsent(Consent $consent): void
98
    {
99
        $this->consent = $consent;
100
    }
101
102
103
    /**
104
     * @param \Symfony\Component\HttpFoundation\Request $request The current request.
105
     *
106
     * @return \SimpleSAML\XHTML\Template
107
     */
108
    public function main(Request $request): Template
109
    {
110
        $authority = $this->moduleConfig->getValue('authority');
111
112
        $as = new $this->authSimple($authority);
113
114
        // If request is a logout request
115
        $logout = $request->get('logout');
116
        if ($logout !== null) {
117
            $returnURL = $this->moduleConfig->getValue('returnURL');
118
            $as->logout($returnURL);
119
        }
120
121
        $hashAttributes = $this->moduleConfig->getValue('attributes.hash', false);
122
123
        $excludeAttributes = $this->moduleConfig->getValue('attributes.exclude', []);
124
125
        // Check if valid local session exists
126
        $as->requireAuth();
127
128
        // Get released attributes
129
        $attributes = $as->getAttributes();
130
131
        // Get metadata storage handler
132
        $metadata = $this->metadataStorageHandler;
133
134
        /*
135
         * Get IdP id and metadata
136
         */
137
        $idp_entityid = $metadata->getMetaDataCurrentEntityID('saml20-idp-hosted');
138
        $idp_metadata = $metadata->getMetaData($idp_entityid, 'saml20-idp-hosted');
139
140
        // Calc correct source
141
        if ($as->getAuthData('saml:sp:IdP') !== null) {
142
            // from a remote idp (as bridge)
143
            $source = 'saml20-idp-remote|' . $as->getAuthData('saml:sp:IdP');
144
        } else {
145
            // from the local idp
146
            $source = $idp_metadata['metadata-set'] . '|' . $idp_entityid;
147
        }
148
149
        // Get user ID
150
        if (isset($idp_metadata['userid.attribute']) && is_string($idp_metadata['userid.attribute'])) {
151
            $userid_attributename = $idp_metadata['userid.attribute'];
152
        } else {
153
            $userid_attributename = 'eduPersonPrincipalName';
154
        }
155
156
        $userids = $attributes[$userid_attributename];
157
158
        if (empty($userids)) {
159
            throw new Exception(sprintf(
160
                'Could not generate useridentifier for storing consent. Attribute [%s] was not available.',
161
                $userid_attributename
162
            ));
163
        }
164
165
        $userid = $userids[0];
166
167
        // Get all SP metadata
168
        $all_sp_metadata = $metadata->getList('saml20-sp-remote');
169
170
        $sp_entityid = $request->get('cv');;
171
        $action = $request->get('action');
172
173
        Logger::critical('consentAdmin: sp: ' . $sp_entityid . ' action: ' . $action);
174
175
        // Remove services, whitch have consent disabled
176
        if (isset($idp_metadata['consent.disable'])) {
177
            foreach ($idp_metadata['consent.disable'] as $disable) {
178
                if (array_key_exists($disable, $all_sp_metadata)) {
179
                    unset($all_sp_metadata[$disable]);
180
                }
181
            }
182
        }
183
184
        Logger::info('consentAdmin: ' . $idp_entityid);
185
186
        // Parse consent config
187
        $consent_storage = Store::parseStoreConfig($this->moduleConfig->getValue('consentadmin'));
188
189
        // Calc correct user ID hash
190
        $hashed_user_id = $this->consent::getHashedUserID($userid, $source);
191
192
        // If a checkbox have been clicked
193
        if ($action !== null && $sp_entityid !== null) {
194
            // init template to enable translation of status messages
195
            $template = new Template(
196
                $this->config,
197
                'consentAdmin:consentadminajax.twig',
198
                'consentAdmin:consentadmin'
199
            );
200
            $translator = $template->getTranslator();
0 ignored issues
show
Unused Code introduced by
The assignment to $translator is dead and can be removed.
Loading history...
201
202
            // Get SP metadata
203
            $sp_metadata = $metadata->getMetaData($sp_entityid, 'saml20-sp-remote');
204
205
            // Run AuthProc filters
206
            list($targeted_id, $attribute_hash, $attributes_new) = $this->driveProcessingChain(
207
                $idp_metadata,
208
                $source,
209
                $sp_metadata,
210
                $sp_entityid,
211
                $attributes,
212
                $userid,
213
                $hashAttributes,
214
                $excludeAttributes
215
            );
216
217
            // Add a consent (or update if attributes have changed and old consent for SP and IdP exists)
218
            if ($action == 'true') {
219
                $isStored = $consent_storage->saveConsent($hashed_user_id, $targeted_id, $attribute_hash);
220
            } else {
221
                if ($action == 'false') {
222
                    // Got consent, so this is a request to remove it
223
                    $consent_storage->deleteConsent($hashed_user_id, $targeted_id);
224
                    $isStored = false;
225
                } else {
226
                    Logger::info('consentAdmin: unknown action');
227
                    $isStored = null;
228
                }
229
            }
230
            $template->data['isStored'] = $isStored;
231
            return $template;
232
        }
233
234
        // Get all consents for user
235
        $user_consent_list = $consent_storage->getConsents($hashed_user_id);
236
237
        // Parse list of consents
238
        $user_consent = [];
239
        foreach ($user_consent_list as $c) {
240
           $user_consent[$c[0]] = $c[1];
241
        }
242
243
        $template_sp_content = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $template_sp_content is dead and can be removed.
Loading history...
244
245
        // Init template
246
        $template = new Template($this->config, 'consentAdmin:consentadmin.twig', 'consentAdmin:consentadmin');
247
        $translator = $template->getTranslator();
248
        $translator->includeLanguageFile('attributes'); // attribute listings translated by this dictionary
249
250
        $sp_empty_description = $translator->getTag('sp_empty_description');
251
        $sp_list = [];
252
253
        // Process consents for all SP
254
        foreach ($all_sp_metadata as $sp_entityid => $sp_values) {
255
            // Get metadata for SP
256
            $sp_metadata = $metadata->getMetaData($sp_entityid, 'saml20-sp-remote');
257
258
            // Run attribute filters
259
            list($targeted_id, $attribute_hash, $attributes_new) = $this->driveProcessingChain(
260
                $idp_metadata,
261
                $source,
262
                $sp_metadata,
263
                $sp_entityid,
264
                $attributes,
265
                $userid,
266
                $hashAttributes,
267
                $excludeAttributes
268
            );
269
270
            // Translate attribute-names
271
            foreach ($attributes_new as $orig_name => $value) {
272
                if (isset($template->data['attribute_' . htmlspecialchars(strtolower($orig_name))])) {
273
                    $old_name = $template->data['attribute_' . htmlspecialchars(strtolower($orig_name))];
0 ignored issues
show
Unused Code introduced by
The assignment to $old_name is dead and can be removed.
Loading history...
274
                }
275
                $name = $translator->getAttributeTranslation(strtolower($orig_name)); // translate
0 ignored issues
show
Bug introduced by
The method getAttributeTranslation() does not exist on SimpleSAML\Locale\Translate. ( Ignorable by Annotation )

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

275
                /** @scrutinizer ignore-call */ 
276
                $name = $translator->getAttributeTranslation(strtolower($orig_name)); // translate

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
276
277
                $attributes_new[$name] = $value;
278
                unset($attributes_new[$orig_name]);
279
            }
280
281
            // Check if consent exists
282
            if (array_key_exists($targeted_id, $user_consent)) {
283
                $sp_status = "changed";
284
                Logger::info('consentAdmin: changed');
285
                // Check if consent is valid. (Possible that attributes has changed)
286
                if ($user_consent[$targeted_id] == $attribute_hash) {
287
                    Logger::info('consentAdmin: ok');
288
                    $sp_status = "ok";
289
                }
290
                // Consent does not exist
291
            } else {
292
                Logger::info('consentAdmin: none');
293
                $sp_status = "none";
294
            }
295
296
            // Set name of SP
297
            if (isset($sp_values['name']) && is_array($sp_values['name'])) {
298
                $sp_name = $sp_metadata['name'];
299
            } else {
300
                if (isset($sp_values['name']) && is_string($sp_values['name'])) {
301
                    $sp_name = $sp_metadata['name'];
302
                } elseif (isset($sp_values['OrganizationDisplayName']) && is_array($sp_values['OrganizationDisplayName'])) {
303
                    $sp_name = $sp_metadata['OrganizationDisplayName'];
304
                }
305
            }
306
307
            // Set description of SP
308
            if (empty($sp_metadata['description']) || !is_array($sp_metadata['description'])) {
309
                $sp_description = $sp_empty_description;
310
            } else {
311
                $sp_description = $sp_metadata['description'];
312
            }
313
314
            // Add a URL to the service if present in metadata
315
            $sp_service_url = isset($sp_metadata['ServiceURL']) ? $sp_metadata['ServiceURL'] : null;
316
317
            // Fill out array for the template
318
            $sp_list[$sp_entityid] = [
319
                'spentityid'       => $sp_entityid,
320
                'name'             => $sp_name,
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $sp_name does not seem to be defined for all execution paths leading up to this point.
Loading history...
321
                'description'      => $sp_description,
322
                'consentStatus'    => $sp_status,
323
                'consentValue'     => $sp_entityid,
324
                'attributes_by_sp' => $attributes_new,
325
                'serviceurl'       => $sp_service_url,
326
            ];
327
        }
328
329
        $template->data['header'] = 'Consent Administration';
330
        $template->data['spList'] = $sp_list;
331
        $template->data['showDescription'] = $this->moduleConfig->getValue('showDescription');
332
333
        return $template;
334
    }
335
336
337
    /**
338
     * Runs the processing chain and ignores all filter which have user
339
     * interaction.
340
     *
341
     * @param array $idp_metadata
342
     * @param string $source
343
     * @param array $sp_metadata
344
     * @param string $sp_entityid
345
     * @param array $attributes
346
     * @param string $userid
347
     * @param bool $hashAttributes
348
     * @param array $excludeAttributes
349
     * @return array
350
     */
351
    private function driveProcessingChain(
352
        array $idp_metadata,
353
        string $source,
354
        array $sp_metadata,
355
        string $sp_entityid,
356
        array $attributes,
357
        string $userid,
358
        bool $hashAttributes,
359
        array $excludeAttributes
360
    ): array {
361
        /*
362
         * Create a new processing chain
363
         */
364
        $pc = new Auth\ProcessingChain($idp_metadata, $sp_metadata, 'idp');
365
366
        /*
367
         * Construct the state.
368
         * REMEMBER: Do not set Return URL if you are calling processStatePassive
369
         */
370
        $authProcState = [
371
            'Attributes'  => $attributes,
372
            'Destination' => $sp_metadata,
373
            'SPMetadata'  => $sp_metadata,
374
            'Source'      => $idp_metadata,
375
            'IdPMetadata' => $idp_metadata,
376
            'isPassive'   => true,
377
        ];
378
379
        /* we're being bridged, so add that info to the state */
380
        if (strpos($source, '-idp-remote|') !== false) {
381
            /** @var int $i */
382
            $i = strpos($source, '|');
383
            $authProcState['saml:sp:IdP'] = substr($source, $i + 1);
384
        }
385
386
        /*
387
         * Call processStatePAssive.
388
         * We are not interested in any user interaction, only modifications to the attributes
389
         */
390
        $pc->processStatePassive($authProcState);
391
392
        $attributes = $authProcState['Attributes'];
393
        // Remove attributes that do not require consent/should be excluded
394
        foreach ($attributes as $attrkey => $attrval) {
395
            if (in_array($attrkey, $excludeAttributes)) {
396
                unset($attributes[$attrkey]);
397
            }
398
        }
399
400
        /*
401
         * Generate identifiers and hashes
402
         */
403
        $destination = $sp_metadata['metadata-set'] . '|' . $sp_entityid;
404
405
        $targeted_id = $this->consent::getTargetedID($userid, $source, $destination);
406
        $attribute_hash = $this->consent::getAttributeHash($attributes, $hashAttributes);
407
408
        Logger::info('consentAdmin: user: ' . $userid);
409
        Logger::info('consentAdmin: target: ' . $targeted_id);
410
        Logger::info('consentAdmin: attribute: ' . $attribute_hash);
411
412
        // Return values
413
        return [$targeted_id, $attribute_hash, $attributes];
414
    }
415
}
416