Passed
Push — release_2_1 ( 8e2e74...55cceb )
by Tomasz
10:20
created

ProfileSilverbullet::getUserAuthRecords()   B

Complexity

Conditions 8
Paths 15

Size

Total Lines 34
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 23
dl 0
loc 34
rs 8.4444
c 1
b 0
f 0
cc 8
nc 15
nop 2
1
<?php
2
3
/*
4
 * Contributions to this work were made on behalf of the GÉANT project, a 
5
 * project that has received funding from the European Union’s Horizon 2020 
6
 * research and innovation programme under Grant Agreement No. 731122 (GN4-2).
7
 * 
8
 * On behalf of the GÉANT project, GEANT Association is the sole owner of the 
9
 * copyright in all material which was developed by a member of the GÉANT 
10
 * project. GÉANT Vereniging (Association) is registered with the Chamber of 
11
 * Commerce in Amsterdam with registration number 40535155 and operates in the
12
 * UK as a branch of GÉANT Vereniging. 
13
 * 
14
 * Registered office: Hoekenrode 3, 1102BR Amsterdam, The Netherlands. 
15
 * UK branch address: City House, 126-130 Hills Road, Cambridge CB2 1PQ, UK
16
 * 
17
 * License: see the web/copyright.inc.php file in the file structure or
18
 *          <base_url>/copyright.php after deploying the software
19
 */
20
21
/**
22
 * This file contains the ProfileSilverbullet class.
23
 *
24
 * @author Stefan Winter <[email protected]>
25
 * @author Tomasz Wolniewicz <[email protected]>
26
 *
27
 * @package Developer
28
 *
29
 */
30
31
namespace core;
32
33
use \Exception;
0 ignored issues
show
Bug introduced by
The type \Exception was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
34
35
/**
36
 * Silverbullet (marketed as "Managed IdP") is a RADIUS profile which 
37
 * corresponds directly to a built-in RADIUS server and CA. 
38
 * It provides all functions needed for a admin-side web interface where users
39
 * can be added and removed, and new devices be enabled.
40
 * 
41
 * When downloading a Silverbullet based profile, the profile includes per-user
42
 * per-device client certificates which can be immediately used to log into 
43
 * eduroam.
44
 *
45
 * @author Stefan Winter <[email protected]>
46
 * @author Tomasz Wolniewicz <[email protected]>
47
 *
48
 * @license see LICENSE file in root directory
49
 *
50
 * @package Developer
51
 */
52
class ProfileSilverbullet extends AbstractProfile {
53
54
    const SB_ACKNOWLEDGEMENT_REQUIRED_DAYS = 365;
55
56
    /**
57
     * terms and conditions for use of this functionality
58
     * 
59
     * @var string
60
     */
61
    public $termsAndConditions;
62
63
    /**
64
     * the displayed name of this feature
65
     */
66
    const PRODUCTNAME = \config\ConfAssistant::SILVERBULLET['subproduct_idp_name'];
67
68
    /**
69
     * Class constructor for existing profiles (use IdP::newProfile() to actually create one). Retrieves all attributes and 
70
     * supported EAP types from the DB and stores them in the priv_ arrays.
71
     * 
72
     * @param int $profileId identifier of the profile in the DB
73
     * @param IdP $idpObject optionally, the institution to which this Profile belongs. Saves the construction of the IdP instance. If omitted, an extra query and instantiation is executed to find out.
74
     */
75
    public function __construct($profileId, $idpObject = NULL) {
76
        parent::__construct($profileId, $idpObject);
77
78
        $this->entityOptionTable = "profile_option";
79
        $this->entityIdColumn = "profile_id";
80
        $this->attributes = [];
81
82
        $tempMaxUsers = 200; // absolutely last resort fallback if no per-fed and no config option
83
// set to global config value
84
85
        if (isset(\config\ConfAssistant::SILVERBULLET['default_maxusers'])) {
86
            $tempMaxUsers = \config\ConfAssistant::SILVERBULLET['default_maxusers'];
87
        }
88
        $myInst = new IdP($this->institution);
89
        $myFed = new Federation($myInst->federation);
90
        $fedMaxusers = $myFed->getAttributes("fed:silverbullet-maxusers");
91
        if (isset($fedMaxusers[0])) {
92
            $tempMaxUsers = $fedMaxusers[0]['value'];
93
        }
94
95
// realm is automatically calculated, then stored in DB
96
97
        $this->realm = "opaquehash@$myInst->identifier-$this->identifier." . strtolower($myInst->federation) . \config\ConfAssistant::SILVERBULLET['realm_suffix'];
98
        $localValueIfAny = "";
99
100
// but there's some common internal attributes populated directly
101
        $internalAttributes = [
102
            "internal:profile_count" => $this->idpNumberOfProfiles,
103
            "internal:realm" => preg_replace('/^.*@/', '', $this->realm),
104
            "internal:use_anon_outer" => FALSE,
105
            "internal:checkuser_outer" => TRUE,
106
            "internal:checkuser_value" => "anonymous",
107
            "internal:anon_local_value" => $localValueIfAny,
108
            "internal:silverbullet_maxusers" => $tempMaxUsers,
109
            "profile:production" => "on",
110
        ];
111
112
        // and we need to populate eap:server_name and eap:ca_file with the NRO-specific EAP information
113
        $silverbulletAttributes = [
114
            "eap:server_name" => "auth." . strtolower($myFed->tld) . \config\ConfAssistant::SILVERBULLET['server_suffix'],
115
        ];
116
        $temp = array_merge($this->addInternalAttributes($internalAttributes), $this->addInternalAttributes($silverbulletAttributes));
117
        $x509 = new \core\common\X509();
118
        $caHandle = fopen(dirname(__FILE__) . "/../config/SilverbulletServerCerts/" . strtoupper($myFed->tld) . "/root.pem", "r");
119
        if ($caHandle !== FALSE) {
120
            $cAFile = fread($caHandle, 16000000);
121
            foreach ($x509->splitCertificate($cAFile) as $oneCa) {
122
                $temp = array_merge($temp, $this->addInternalAttributes(['eap:ca_file' => $oneCa]));
123
            }
124
        }
125
126
        $tempArrayProfLevel = array_merge($this->addDatabaseAttributes(), $temp);
127
128
// now, fetch and merge IdP-wide attributes
129
130
        $this->attributes = $this->levelPrecedenceAttributeJoin($tempArrayProfLevel, $this->idpAttributes, "IdP");
131
132
        $this->privEaptypes = $this->fetchEAPMethods();
133
134
        $this->name = ProfileSilverbullet::PRODUCTNAME;
135
136
        $this->loggerInstance->debug(4, "--- END Constructing new Profile object ... ---\n");
137
138
        $product = \core\ProfileSilverbullet::PRODUCTNAME;
139
        $nameIdP = \config\ConfAssistant::CONSORTIUM['nomenclature_idp'];
140
        $nameConsortium = \config\ConfAssistant::CONSORTIUM['display_name'];
141
        $nameFed = \config\ConfAssistant::CONSORTIUM['nomenclature_federation'];
142
        $this->termsAndConditions = "<h2>Product Definition</h2>
143
        <p>$product outsources the technical setup of $nameConsortium $nameIdP functions to the $nameConsortium Operations Team. The system includes</p>
144
            <ul>
145
                <li>a web-based user management interface where user accounts and access credentials can be created and revoked (there is a limit to the number of active users)</li>
146
                <li>a technical infrastructure ('CA') which issues and revokes credentials</li>
147
                <li>a technical infrastructure ('RADIUS') which verifies access credentials and subsequently grants access to $nameConsortium</li>           
148
            </ul>
149
        <h2>User Account Liability</h2>
150
        <p>As an $nameConsortium $nameIdP administrator using this system, you are authorised to create user accounts according to your local $nameIdP policy. You are fully responsible for the accounts you issue and are the data controller for all user information you deposit in this system; the system is a data processor.</p>";
151
        $this->termsAndConditions .= "<p>Your responsibilities include that you</p>
152
        <ul>
153
            <li>only issue accounts to members of your $nameIdP, as defined by your local policy.</li>
154
            <li>must make sure that all accounts that you issue can be linked by you to actual human end users</li>
155
            <li>have to immediately revoke accounts of users when they leave or otherwise stop being a member of your $nameIdP</li>
156
            <li>will act upon notifications about possible network abuse by your users and will appropriately sanction them</li>
157
        </ul>
158
        <p>";
159
        $this->termsAndConditions .= "Failure to comply with these requirements may make your $nameFed act on your behalf, which you authorise, and will ultimately lead to the deletion of your $nameIdP (and all the users you create inside) in this system.";
160
        $this->termsAndConditions .= "</p>
161
        <h2>Privacy</h2>
162
        <p>With $product, we are necessarily storing personally identifiable information about the end users you create. While the actual human is only identifiable with your help, we consider all the user data as relevant in terms of privacy jurisdiction. Please note that</p>
163
        <ul>
164
            <li>You are the only one who needs to be able to make a link to the human behind the usernames you create. The usernames you create in the system have to be rich enough to allow you to make that identification step. Also consider situations when you are unavailable or leave the organisation and someone else needs to perform the matching to an individual.</li>
165
            <li>The identifiers we create in the credentials are not linked to the usernames you add to the system; they are randomly generated pseudonyms.</li>
166
            <li>Each access credential carries a different pseudonym, even if it pertains to the same username.</li>
167
            <li>If you choose to deposit users' email addresses in the system, you authorise the system to send emails on your behalf regarding operationally relevant events to the users in question (e.g. notification of nearing expiry dates of credentials, notification of access revocation).
168
        </ul>";
169
    }
170
171
    /**
172
     * Updates database with new installer location; NOOP because we do not
173
     * cache anything in Silverbullet
174
     * 
175
     * @param string $device         the device identifier string
176
     * @param string $path           the path where the new installer can be found
177
     * @param string $mime           the mime type of the new installer
178
     * @param int    $integerEapType the inter-representation of the EAP type that is configured in this installer
179
     * @return void
180
     * @throws Exception
181
     */
182
    public function updateCache($device, $path, $mime, $integerEapType, $openRoaming) {
183
        // caching is not supported in SB (private key in installers)
184
        // the following merely makes the "unused parameter" warnings go away
185
        // the FALSE in condition one makes sure it never gets executed
186
        if (FALSE || $device == "Macbeth" || $path == "heath" || $mime == "application/witchcraft" || $integerEapType == 0) {
187
            throw new Exception("FALSE is TRUE, and TRUE is FALSE! Hover through the browser and filthy code!");
188
        }
189
    }
190
191
    /**
192
     * register new supported EAP method for this profile
193
     *
194
     * @param \core\common\EAP $type       The EAP Type, as defined in class EAP
195
     * @param int              $preference preference of this EAP Type. If a preference value is re-used, the order of EAP types of the same preference level is undefined.
196
     * @return void
197
     * @throws Exception
198
     */
199
    public function addSupportedEapMethod(\core\common\EAP $type, $preference) {
200
        // the parameters really should only list SB and with prio 1 - otherwise,
201
        // something fishy is going on
202
        if ($type->getIntegerRep() != \core\common\EAP::INTEGER_SILVERBULLET || $preference != 1) {
203
            throw new Exception("Silverbullet::addSupportedEapMethod was called for a non-SP EAP type or unexpected priority!");
204
        }
205
        parent::addSupportedEapMethod($type, 1);
206
    }
207
208
    /**
209
     * It's EAP-TLS and there is no point in anonymity
210
     * @param boolean $shallwe always FALSE
211
     * @return void
212
     * @throws Exception
213
     */
214
    public function setAnonymousIDSupport($shallwe) {
215
        // we don't do anonymous outer IDs in SB
216
        if ($shallwe === TRUE) {
217
            throw new Exception("Silverbullet: attempt to add anonymous outer ID support to a SB profile!");
218
        }
219
        $this->databaseHandle->exec("UPDATE profile SET use_anon_outer = 0 WHERE profile_id = $this->identifier");
220
    }
221
222
    /**
223
     * find out about the status of a given SB user; retrieves the info regarding all his tokens (and thus all his certificates)
224
     * @param int $userId the userid
225
     * @return array of invitationObjects
226
     */
227
    public function userStatus($userId) {
228
        $retval = [];
229
        $userrows = $this->databaseHandle->exec("SELECT `token` FROM `silverbullet_invitation` WHERE `silverbullet_user_id` = ? AND `profile_id` = ? ", "ii", $userId, $this->identifier);
230
        // SELECT -> resource, not boolean
231
        while ($returnedData = mysqli_fetch_object(/** @scrutinizer ignore-type */ $userrows)) {
232
            $retval[] = new SilverbulletInvitation($returnedData->token);
233
        }
234
        return $retval;
235
    }
236
237
    /**
238
     * finds out the expiry date of a given user
239
     * @param int $userId the numerical user ID of the user in question
240
     * @return string
241
     */
242
    public function getUserExpiryDate($userId) {
243
        $query = $this->databaseHandle->exec("SELECT expiry FROM silverbullet_user WHERE id = ? AND profile_id = ? ", "ii", $userId, $this->identifier);
244
        // SELECT -> resource, not boolean
245
        while ($returnedData = mysqli_fetch_object(/** @scrutinizer ignore-type */ $query)) {
246
            return $returnedData->expiry;
247
        }
248
    }
249
    
250
    /**
251
     * retrieves the authentication records from the RADIUS servers 
252
     * 
253
     * @param int $userId the numerical user ID of the user in question
254
     * @param boolean $testActivity set true if we are only interested in checking
255
     *        user existance in auth database
256
     * @return array
257
     */
258
    public function getUserAuthRecords($userId, $testActivity = false) {
259
        // find out all certificate CNs belonging to the user, including expired and revoked ones
260
        $userData = $this->userStatus($userId);
261
        $certNames = [];
262
        foreach ($userData as $oneSlice) {
263
            foreach ($oneSlice->associatedCertificates as $oneCert) {
264
                $certNames[] = $oneCert->username;
265
            }
266
        }
267
        if (empty($certNames)) {
268
            return [];
269
        }
270
        $namesCondensed = "'" . implode("' OR username = '", $certNames) . "'";
271
        $serverHandles = DBConnection::handle("RADIUS");
272
        $returnarray = [];
273
        foreach ($serverHandles as $oneDbServer) {
274
            if ($testActivity === true) {
275
                $query = $oneDbServer->exec("SELECT username FROM eduroamauth WHERE username = $namesCondensed");
276
                $row_cnt = $query->num_rows;
277
                if ($row_cnt !== 0) {
278
                    return [1];
279
                }
280
            } else {
281
                $query = $oneDbServer->exec("SELECT username, authdate, reply, callingid, operatorname FROM eduroamauth WHERE username = $namesCondensed ORDER BY authdate DESC");
282
            // SELECT -> resource, not boolean
283
                while ($returnedData = mysqli_fetch_object(/** @scrutinizer ignore-type */ $query)) {
284
                    $returnarray[] = ["CN" => $returnedData->username, "TIMESTAMP" => $returnedData->authdate, "RESULT" => $returnedData->reply, "MAC" => $returnedData->callingid, "OPERATOR" => $returnedData->operatorname];
285
                }
286
            }
287
        }
288
        usort($returnarray, function($one, $another) {
289
            return $one['TIMESTAMP'] < $another['TIMESTAMP'];
290
        });
291
        return $returnarray;
292
    }
293
294
    /**
295
     * sets the expiry date of a user to a new date of choice
296
     * @param int       $userId the username
297
     * @param \DateTime $date   the expiry date
298
     * @return void
299
     */
300
    public function setUserExpiryDate($userId, $date) {
301
        $query = "UPDATE silverbullet_user SET expiry = ? WHERE profile_id = ? AND id = ?";
302
        $theDate = $date->format("Y-m-d H:i:s");
303
        $this->databaseHandle->exec($query, "sii", $theDate, $this->identifier, $userId);
304
    }
305
306
    /**
307
     * lists all users of this SB profile
308
     * @return array
309
     */
310
    public function listAllUsers() {
311
        $userArray = [];
312
        $users = $this->databaseHandle->exec("SELECT `id`, `username` FROM `silverbullet_user` WHERE `profile_id` = ? ", "i", $this->identifier);
313
        // SELECT -> resource, not boolean
314
        while ($res = mysqli_fetch_object(/** @scrutinizer ignore-type */ $users)) {
315
            $userArray[$res->id] = $res->username;
316
        }
317
        return $userArray;
318
    }
319
320
    /**
321
     * get the user of this SB profile identified by ID
322
     * @param int $userId the user id
323
     * @return array
324
     */
325
    public function getUserById($userId) {
326
        $users = $this->databaseHandle->exec("SELECT `id`, `username` FROM `silverbullet_user` WHERE `profile_id` = ? AND `id` = ? ", "ii", $this->identifier, $userId);
327
        // SELECT -> resource, not boolean
328
        while ($res = mysqli_fetch_object(/** @scrutinizer ignore-type */ $users)) {
329
            return [$res->id => $res->username];
330
        }
331
        return [];
332
    }
333
334
    /**
335
     * get the user of this SB profile identified by Username
336
     * @param string $userName the username
337
     * @return array
338
     */
339
    public function getUserByName($userName) {
340
        $users = $this->databaseHandle->exec("SELECT `id`, `username` FROM `silverbullet_user` WHERE `profile_id` = ? AND `username` = ? ", "is", $this->identifier, $userName);
341
        // SELECT -> resource, not boolean
342
        while ($res = mysqli_fetch_object(/** @scrutinizer ignore-type */ $users)) {
343
            return [$res->id => $res->username];
344
        }
345
        return [];
346
    }
347
348
    /**
349
     * lists all users which are currently active (i.e. have pending invitations and/or valid certs)
350
     * @return array
351
     */
352
    public function listActiveUsers() {
353
        // users are active if they have a non-expired invitation OR a non-expired, non-revoked certificate
354
        $userCount = [];
355
        $users = $this->databaseHandle->exec("SELECT DISTINCT u.id AS usercount FROM "
356
                . "silverbullet_user u JOIN silverbullet_invitation i ON u.id = i.silverbullet_user_id "
357
                . "JOIN silverbullet_certificate c ON u.id = c.silverbullet_user_id "
358
                . "WHERE u.profile_id = ? "
359
                . "AND ( "
360
                . "( u.id = i.silverbullet_user_id AND i.expiry >= NOW() )"
361
                . "     OR"
362
                . "  ( u.id = c.silverbullet_user_id AND c.expiry >= NOW() AND c.revocation_status != 'REVOKED' ) "
363
                . ")", "i", $this->identifier);
364
      // SELECT -> resource, not boolean
365
        while ($res = mysqli_fetch_object(/** @scrutinizer ignore-type */ $users)) {
366
            $userCount[$res->usercount] = "ACTIVE";
367
        }
368
        return $userCount;
369
    }
370
371
    /**
372
     * adds a new user to the profile
373
     * 
374
     * @param string    $username the username
375
     * @param \DateTime $expiry   the expiry date
376
     * @return int row_id ID of the new user in the database
377
     */
378
    public function addUser($username, \DateTime $expiry) {
379
        $query = "INSERT INTO silverbullet_user (profile_id, username, expiry) VALUES(?,?,?)";
380
        $date = $expiry->format("Y-m-d H:i:s");
381
        $this->databaseHandle->exec($query, "iss", $this->identifier, $username, $date);
382
        return $this->databaseHandle->lastID();
383
    }
384
385
    /**
386
     * revoke all active certificates and pending invitations of a user
387
     * @param int $userId the username
388
     * @return boolean was the user found and deactivated?
389
     * @throws Exception
390
     */
391
    public function deactivateUser($userId) {
392
        // does the user exist and is active, anyway?
393
        $queryExisting = "SELECT id FROM silverbullet_user WHERE profile_id = $this->identifier AND id = ? AND expiry >= NOW()";
394
        $execExisting = $this->databaseHandle->exec($queryExisting, "i", $userId);
395
        // this is a SELECT, and won't return TRUE
396
        if (mysqli_num_rows(/** @scrutinizer ignore-type */ $execExisting) < 1) {
397
            return FALSE;
398
        }
399
        // set the expiry date of any still valid invitations to NOW()
400
        $query = "SELECT token FROM silverbullet_invitation WHERE profile_id = $this->identifier AND silverbullet_user_id = ? AND expiry >= NOW()";
401
        $exec = $this->databaseHandle->exec($query, "i", $userId);
402
        // SELECT -> resource, not boolean
403
        while ($result = mysqli_fetch_object(/** @scrutinizer ignore-type */ $exec)) {
404
            $invitation = new SilverbulletInvitation($result->token);
405
            $invitation->revokeInvitation();
406
        }
407
        // and revoke all certificates
408
        $query2 = "SELECT serial_number, ca_type FROM silverbullet_certificate WHERE profile_id = $this->identifier AND silverbullet_user_id = ? AND expiry >= NOW() AND revocation_status = 'NOT_REVOKED'";
409
        $exec2 = $this->databaseHandle->exec($query2, "i", $userId);
410
        // SELECT -> resource, not boolean
411
        while ($result = mysqli_fetch_object(/** @scrutinizer ignore-type */ $exec2)) {
412
            $certObject = new SilverbulletCertificate($result->serial_number, $result->ca_type);
413
            $certObject->revokeCertificate();
414
        }
415
        // and finally set the user expiry date to NOW(), too
416
        $query3 = "UPDATE silverbullet_user SET expiry = NOW() WHERE profile_id = $this->identifier AND id = ?";
417
        $ret = $this->databaseHandle->exec($query3, "i", $userId);
418
        // this is an UPDATE, and always returns TRUE. Need to tell Scrutinizer all about it.
419
        if ($ret === TRUE) {
420
            return TRUE;
421
        } else {
422
            throw new Exception("The UPDATE statement could not be executed successfully.");
423
        }
424
    }
425
426
    /**
427
     * delete the user in question, including all expired and revoked certificates
428
     * @param int $userId the username
429
     * @return boolean was the user deleted?
430
     */
431
    public function deleteUser($userId) {
432
        // do we really not have any auth records that may need to be tied to this user?
433
        if (count($this->getUserAuthRecords($userId)) > 0) {
434
            return false;
435
        }
436
        // find and delete all certificates
437
        $certQuery = "DELETE FROM silverbullet_certificate WHERE profile_id = $this->identifier AND silverbullet_user_id = ?";
438
        $this->databaseHandle->exec($certQuery, "i", $userId);
439
        // find and delete obsolete invitation token track record
440
        $tokenQuery = "DELETE FROM silverbullet_invitation WHERE profile_id = $this->identifier AND silverbullet_user_id = ?";
441
        $this->databaseHandle->exec($tokenQuery, "i", $userId);
442
        // delete username to cert mappings
443
        $radiusQuery = "DELETE FROM radcheck WHERE username = ?";
444
        $radiusDbs = DBConnection::handle("RADIUS");
445
        foreach ($radiusDbs as $oneRadiusDb) {
446
            $oneRadiusDb->exec($radiusQuery, "s", ($this->getUserById($userId))[$userId]);
447
        }
448
        // delete user record itself
449
        $userQuery = "DELETE FROM silverbullet_user WHERE profile_id = $this->identifier AND id = ?";
450
        $this->databaseHandle->exec($userQuery, "i", $userId);
451
    }
452
453
    /**
454
     * updates the last_ack for all users (invoked when the admin claims to have re-verified continued eligibility of all users)
455
     * 
456
     * @return void
457
     */
458
    public function refreshEligibility() {
459
        $query = "UPDATE silverbullet_user SET last_ack = NOW() WHERE profile_id = ?";
460
        $this->databaseHandle->exec($query, "i", $this->identifier);
461
    }
462
463
}
464