Passed
Push — master ( 8305a9...adc7e8 )
by Stefan
06:45
created

ProfileSilverbullet::deactivateUser()   B

Complexity

Conditions 4
Paths 5

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 25
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 15
nc 5
nop 1
1
<?php
2
3
/*
4
 * ******************************************************************************
5
 * Copyright 2011-2017 DANTE Ltd. and GÉANT on behalf of the GN3, GN3+, GN4-1 
6
 * and GN4-2 consortia
7
 *
8
 * License: see the web/copyright.php file in the file structure
9
 * ******************************************************************************
10
 */
11
12
/**
13
 * This file contains the ProfileSilverbullet class.
14
 *
15
 * @author Stefan Winter <[email protected]>
16
 * @author Tomasz Wolniewicz <[email protected]>
17
 *
18
 * @package Developer
19
 *
20
 */
21
22
namespace core;
23
24
use \Exception;
25
26
/**
27
 * Silverbullet (marketed as "Managed IdP") is a RADIUS profile which 
28
 * corresponds directly to a built-in RADIUS server and CA. 
29
 * It provides all functions needed for a admin-side web interface where users
30
 * can be added and removed, and new devices be enabled.
31
 * 
32
 * When downloading a Silverbullet based profile, the profile includes per-user
33
 * per-device client certificates which can be immediately used to log into 
34
 * eduroam.
35
 *
36
 * @author Stefan Winter <[email protected]>
37
 * @author Tomasz Wolniewicz <[email protected]>
38
 *
39
 * @license see LICENSE file in root directory
40
 *
41
 * @package Developer
42
 */
43
class ProfileSilverbullet extends AbstractProfile {
44
45
    const SB_ACKNOWLEDGEMENT_REQUIRED_DAYS = 365;
46
47
    public $termsAndConditions;
48
49
    /*
50
     * 
51
     */
52
53
    const PRODUCTNAME = "Managed IdP";
54
55
    /**
56
     * Class constructor for existing profiles (use IdP::newProfile() to actually create one). Retrieves all attributes and 
57
     * supported EAP types from the DB and stores them in the priv_ arrays.
58
     * 
59
     * @param int $profileId identifier of the profile in the DB
60
     * @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.
61
     */
62
    public function __construct($profileId, $idpObject = NULL) {
63
        parent::__construct($profileId, $idpObject);
64
65
        $this->entityOptionTable = "profile_option";
66
        $this->entityIdColumn = "profile_id";
67
        $this->attributes = [];
68
69
        $tempMaxUsers = 200; // abolutely last resort fallback if no per-fed and no config option
70
// set to global config value
71
72
        if (isset(CONFIG_CONFASSISTANT['SILVERBULLET']['default_maxusers'])) {
73
            $tempMaxUsers = CONFIG_CONFASSISTANT['SILVERBULLET']['default_maxusers'];
74
        }
75
        $myInst = new IdP($this->institution);
76
        $myFed = new Federation($myInst->federation);
77
        $fedMaxusers = $myFed->getAttributes("fed:silverbullet-maxusers");
78
        if (isset($fedMaxusers[0])) {
79
            $tempMaxUsers = $fedMaxusers[0]['value'];
80
        }
81
82
// realm is automatically calculated, then stored in DB
83
84
        $this->realm = "opaquehash@$myInst->identifier-$this->identifier." . strtolower($myInst->federation) . CONFIG_CONFASSISTANT['SILVERBULLET']['realm_suffix'];
85
        $localValueIfAny = "";
86
87
// but there's some common internal attributes populated directly
88
        $internalAttributes = [
89
            "internal:profile_count" => $this->idpNumberOfProfiles,
90
            "internal:realm" => preg_replace('/^.*@/', '', $this->realm),
91
            "internal:use_anon_outer" => FALSE,
92
            "internal:checkuser_outer" => TRUE,
93
            "internal:checkuser_value" => "anonymous",
94
            "internal:anon_local_value" => $localValueIfAny,
95
            "internal:silverbullet_maxusers" => $tempMaxUsers,
96
            "profile:production" => "on",
97
        ];
98
99
// and we need to populate eap:server_name and eap:ca_file with the NRO-specific EAP information
100
        $silverbulletAttributes = [
101
            "eap:server_name" => "auth." . strtolower($myFed->tld) . CONFIG_CONFASSISTANT['SILVERBULLET']['server_suffix'],
102
        ];
103
        $x509 = new \core\common\X509();
104
        $caHandle = fopen(dirname(__FILE__) . "/../config/SilverbulletServerCerts/" . strtoupper($myFed->tld) . "/root.pem", "r");
105
        if ($caHandle !== FALSE) {
106
            $cAFile = fread($caHandle, 16000000);
107
            $silverbulletAttributes["eap:ca_file"] = $x509->der2pem(($x509->pem2der($cAFile)));
108
        }
109
110
        $temp = array_merge($this->addInternalAttributes($internalAttributes), $this->addInternalAttributes($silverbulletAttributes));
111
        $tempArrayProfLevel = array_merge($this->addDatabaseAttributes(), $temp);
112
113
// now, fetch and merge IdP-wide attributes
114
115
        $this->attributes = $this->levelPrecedenceAttributeJoin($tempArrayProfLevel, $this->idpAttributes, "IdP");
116
117
        $this->privEaptypes = $this->fetchEAPMethods();
118
119
        $this->name = ProfileSilverbullet::PRODUCTNAME;
120
121
        $this->loggerInstance->debug(3, "--- END Constructing new Profile object ... ---\n");
122
123
        $this->termsAndConditions = "<h2>Product Definition</h2>
124
        <p>" . \core\ProfileSilverbullet::PRODUCTNAME . " outsources the technical setup of " . CONFIG_CONFASSISTANT['CONSORTIUM']['display_name'] . " " . CONFIG_CONFASSISTANT['CONSORTIUM']['nomenclature_institution'] . " functions to the " . CONFIG_CONFASSISTANT['CONSORTIUM']['display_name'] . " Operations Team. The system includes</p>
125
            <ul>
126
                <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>
127
                <li>a technical infrastructure ('CA') which issues and revokes credentials</li>
128
                <li>a technical infrastructure ('RADIUS') which verifies access credentials and subsequently grants access to " . CONFIG_CONFASSISTANT['CONSORTIUM']['display_name'] . "</li>
129
                <li><span style='color: red;'>TBD: a lookup/notification system which informs you of network abuse complaints by " . CONFIG_CONFASSISTANT['CONSORTIUM']['display_name'] . " Service Providers that pertain to your users</span></li>
130
            </ul>
131
        <h2>User Account Liability</h2>
132
        <p>As an " . CONFIG_CONFASSISTANT['CONSORTIUM']['display_name'] . " " . CONFIG_CONFASSISTANT['CONSORTIUM']['nomenclature_institution'] . " administrator using this system, you are authorized to create user accounts according to your local " . CONFIG_CONFASSISTANT['CONSORTIUM']['nomenclature_institution'] . " 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>";
133
        $this->termsAndConditions .= "<p>Your responsibilities include that you</p>
134
        <ul>
135
            <li>only issue accounts to members of your " . CONFIG_CONFASSISTANT['CONSORTIUM']['nomenclature_institution'] . ", as defined by your local policy.</li>
136
            <li>must make sure that all accounts that you issue can be linked by you to actual human end users</li>
137
            <li>have to immediately revoke accounts of users when they leave or otherwise stop being a member of your " . CONFIG_CONFASSISTANT['CONSORTIUM']['nomenclature_institution'] . "</li>
138
            <li>will act upon notifications about possible network abuse by your users and will appropriately sanction them</li>
139
        </ul>
140
        <p>";
141
        $this->termsAndConditions .= "Failure to comply with these requirements may make your " . CONFIG_CONFASSISTANT['CONSORTIUM']['nomenclature_federation'] . " act on your behalf, which you authorise, and will ultimately lead to the deletion of your " . CONFIG_CONFASSISTANT['CONSORTIUM']['nomenclature_institution'] . " (and all the users you create inside) in this system.";
142
        $this->termsAndConditions .= "</p>
143
        <h2>Privacy</h2>
144
        <p>With " . \core\ProfileSilverbullet::PRODUCTNAME . ", 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>
145
        <ul>
146
            <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>
147
            <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>
148
            <li>Each access credential carries a different pseudonym, even if it pertains to the same username.</li>
149
            <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).
150
        </ul>";
151
    }
152
    
153
    /**
154
     * Updates database with new installer location; NOOP because we do not
155
     * cache anything in Silverbullet
156
     * 
157
     * @param string $device the device identifier string
158
     * @param string $path the path where the new installer can be found
159
     * @param string $mime the mime type of the new installer
160
     * @param int $integerEapType the inter-representation of the EAP type that is configured in this installer
161
     */
162
    public function updateCache($device, $path, $mime, $integerEapType) {
163
        // caching is not supported in SB (private key in installers)
164
        // the following merely makes the "unused parameter" warnings go away
165
        // the FALSE in condition one makes sure it never gets executed
166
        if (FALSE || $device == "Macbeth" || $path == "heath" || $mime == "application/witchcraft" || $integerEapType == 0) {
167
            throw new Exception("FALSE is TRUE, and TRUE is FALSE! Hover through the browser and filthy code!");
168
        }
169
    }
170
171
    /**
172
     * register new supported EAP method for this profile
173
     *
174
     * @param \core\common\EAP $type The EAP Type, as defined in class EAP
175
     * @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.
176
     *
177
     */
178
    public function addSupportedEapMethod(\core\common\EAP $type, $preference) {
179
        // the parameters really should only list SB and with prio 1 - otherwise,
180
        // something fishy is going on
181
        if ($type->getIntegerRep() != \core\common\EAP::INTEGER_SILVERBULLET || $preference != 1) {
182
            throw new Exception("Silverbullet::addSupportedEapMethod was called for a non-SP EAP type or unexpected priority!");
183
        }
184
        parent::addSupportedEapMethod($type, 1);
185
    }
186
187
    /**
188
     * It's EAP-TLS and there is no point in anonymity
189
     * @param boolean $shallwe
190
     */
191
    public function setAnonymousIDSupport($shallwe) {
192
        // we don't do anonymous outer IDs in SB
193
        if ($shallwe === TRUE) {
194
            throw new Exception("Silverbullet: attempt to add anonymous outer ID support to a SB profile!");
195
        }
196
        $this->databaseHandle->exec("UPDATE profile SET use_anon_outer = 0 WHERE profile_id = $this->identifier");
197
    }
198
199
    /**
200
     * performs an HTTP request. Currently unused, will be for external CA API calls.
201
     * 
202
     * @param string $url the URL to send the request to
203
     * @param array $postValues POST values to send
204
     * @return string the returned HTTP content
205
     */
206
    private function httpRequest($url, $postValues) {
0 ignored issues
show
Unused Code introduced by
The method httpRequest() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
207
        $options = [
208
            'http' => ['header' => 'Content-type: application/x-www-form-urlencoded\r\n', "method" => 'POST', 'content' => http_build_query($postValues)]
209
        ];
210
        $context = stream_context_create($options);
211
        return file_get_contents($url, false, $context);
212
    }
213
214
    /**
215
     * find out about the status of a given SB user; retrieves the info regarding all his tokens (and thus all his certificates)
216
     * @param int $userId
217
     * @return array of invitationObjects
218
     */
219
    public function userStatus($userId) {
220
        $retval = [];
221
        $userrows = $this->databaseHandle->exec("SELECT `token` FROM `silverbullet_invitation` WHERE `silverbullet_user_id` = ? AND `profile_id` = ? ", "ii", $userId, $this->identifier);
222
        // SELECT -> resource, not boolean
223
        while ($returnedData = mysqli_fetch_object(/** @scrutinizer ignore-type */ $userrows)) {
224
            $retval[] = new SilverbulletInvitation($returnedData->token);
225
        }
226
        return $retval;
227
    }
228
229
    /**
230
     * finds out the expiry date of a given user
231
     * @param int $userId
232
     * @return string
233
     */
234
    public function getUserExpiryDate($userId) {
235
        $query = $this->databaseHandle->exec("SELECT expiry FROM silverbullet_user WHERE id = ? AND profile_id = ? ", "ii", $userId, $this->identifier);
236
        // SELECT -> resource, not boolean
237
        while ($returnedData = mysqli_fetch_object(/** @scrutinizer ignore-type */ $query)) {
238
            return $returnedData->expiry;
239
        }
240
    }
241
    
242
    /**
243
     * sets the expiry date of a user to a new date of choice
244
     * @param int $userId
245
     * @param \DateTime $date
246
     */
247
    public function setUserExpiryDate($userId, $date) {
248
        $query = "UPDATE silverbullet_user SET expiry = ? WHERE profile_id = ? AND id = ?";
249
        $theDate = $date->format("Y-m-d");
250
        $this->databaseHandle->exec($query, "sii", $theDate, $this->identifier, $userId);
251
    }
252
253
    /**
254
     * lists all users of this SB profile
255
     * @return array
256
     */
257
    public function listAllUsers() {
258
        $userArray = [];
259
        $users = $this->databaseHandle->exec("SELECT `id`, `username` FROM `silverbullet_user` WHERE `profile_id` = ? ", "i", $this->identifier);
260
        // SELECT -> resource, not boolean
261
        while ($res = mysqli_fetch_object(/** @scrutinizer ignore-type */ $users)) {
262
            $userArray[$res->id] = $res->username;
263
        }
264
        return $userArray;
265
    }
266
267
    /**
268
     * lists all users which are currently active (i.e. have pending invitations and/or valid certs)
269
     * @return array
270
     */
271
    public function listActiveUsers() {
272
        // users are active if they have a non-expired invitation OR a non-expired, non-revoked certificate
273
        $userCount = [];
274
        $users = $this->databaseHandle->exec("SELECT DISTINCT u.id AS usercount FROM silverbullet_user u, silverbullet_invitation i, silverbullet_certificate c "
275
                . "WHERE u.profile_id = ? "
276
                . "AND ( "
277
                . "( u.id = i.silverbullet_user_id AND i.expiry >= NOW() )"
278
                . "     OR"
279
                . "  ( u.id = c.silverbullet_user_id AND c.expiry >= NOW() AND c.revocation_status != 'REVOKED' ) "
280
                . ")", "i", $this->identifier);
281
        // SELECT -> resource, not boolean
282
        while ($res = mysqli_fetch_object(/** @scrutinizer ignore-type */ $users)) {
283
            $userCount[] = $res->usercount;
284
        }
285
        return $userCount;
286
    }
287
288
    /**
289
     * adds a new user to the profile
290
     * 
291
     * @param string $username
292
     * @param \DateTime $expiry
293
     * @return int row ID of the new user in the database
294
     */
295
    public function addUser($username, \DateTime $expiry) {
296
        $query = "INSERT INTO silverbullet_user (profile_id, username, expiry) VALUES(?,?,?)";
297
        $date = $expiry->format("Y-m-d");
298
        $this->databaseHandle->exec($query, "iss", $this->identifier, $username, $date);
299
        return $this->databaseHandle->lastID();
300
    }
301
302
    /**
303
     * revoke all active certificates and pending invitations of a user
304
     * @param int $userId
305
     * @return boolean was the user found and deactivated?
306
     */
307
    public function deactivateUser($userId) {
308
        // does the user exist and is active, anyway?
309
        $queryEx = "SELECT id FROM silverbullet_user WHERE profile_id = $this->identifier AND id = ? AND expiry >= NOW()";
310
        if (mysqli_num_rows($queryEx) < 1) {
0 ignored issues
show
Bug introduced by
$queryEx of type string is incompatible with the type mysqli_result expected by parameter $result of mysqli_num_rows(). ( Ignorable by Annotation )

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

310
        if (mysqli_num_rows(/** @scrutinizer ignore-type */ $queryEx) < 1) {
Loading history...
311
            return FALSE;
312
        }
313
        // set the expiry date of any still valid invitations to NOW()
314
        $query = "SELECT id FROM silverbullet_invitation WHERE profile_id = $this->identifier AND silverbullet_user_id = ? AND expiry >= NOW()";
315
        $exec = $this->databaseHandle->exec($query, "i", $userId);
316
        // SELECT -> resource, not boolean
317
        while ($result = mysqli_fetch_object(/** @scrutinizer ignore-type */ $exec)) {
318
            $invitation = new SilverbulletInvitation($result->id);
319
            $invitation->revokeInvitation();
320
        }
321
        // and revoke all certificates
322
        $query2 = "SELECT serial_number FROM silverbullet_certificate WHERE profile_id = $this->identifier AND silverbullet_user_id = ? AND expiry >= NOW() AND revocation_status = 'NOT_REVOKED'";
323
        $exec2 = $this->databaseHandle->exec($query2, "i", $userId);
324
        // SELECT -> resource, not boolean
325
        while ($result = mysqli_fetch_object(/** @scrutinizer ignore-type */ $exec2)) {
326
            $certObject = new SilverbulletCertificate($result->serial_number);
327
            $certObject->revokeCertificate();
328
        }
329
        // and finally set the user expiry date to NOW(), too
330
        $query3 = "UPDATE silverbullet_user SET expiry = NOW() WHERE profile_id = $this->identifier AND id = ?";
331
        return $this->databaseHandle->exec($query3, "i", $userId);
332
    }
333
    
334
    /**
335
     * updates the last_ack for all users (invoked when the admin claims to have re-verified continued eligibility of all users)
336
     */
337
    public function refreshEligibility() {
338
        $query = "UPDATE silverbullet_user SET last_ack = NOW() WHERE profile_id = ?";
339
        $this->databaseHandle->exec($query, "i", $this->identifier);
340
    }
341
}
342