Completed
Push — master ( 34b090...af3ce8 )
by Stefan
04:50
created

UserManagement::listPendingInvitations()   C

Complexity

Conditions 7
Paths 6

Size

Total Lines 22
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 15
c 1
b 0
f 0
nc 6
nop 1
dl 0
loc 22
rs 6.9811
1
<?php
2
3
/* * ********************************************************************************
4
 * (c) 2011-15 GÉANT on behalf of the GN3, GN3plus and GN4 consortia
5
 * License: see the LICENSE file in the root directory
6
 * ********************************************************************************* */
7
?>
8
<?php
9
10
/**
11
 * This file contains the UserManagement class.
12
 *
13
 * @author Stefan Winter <[email protected]>
14
 * @author Tomasz Wolniewicz <[email protected]>
15
 * 
16
 * @license see LICENSE file in root directory
17
 * 
18
 * @package Developer
19
 */
20
/**
21
 * necessary includes
22
 */
23
require_once('DBConnection.php');
24
require_once("Federation.php");
25
require_once("IdP.php");
26
require_once("CAT.php");
27
28
/**
29
 * This class manages user privileges and bindings to institutions
30
 *
31
 * @author Stefan Winter <[email protected]>
32
 * @author Tomasz Wolniewicz <[email protected]>
33
 * 
34
 * @package Developer
35
 */
36
class UserManagement {
37
38
    /**
39
     * Class constructor. Nothing special to be done when constructing.
40
     */
41
    public function __construct() {
42
        
43
    }
44
45
    /**
46
     * database which this class queries by default
47
     * 
48
     * @var string
49
     */
50
    private static $DB_TYPE = "INST";
51
52
    /**
53
     * Checks if a given invitation token exists and is valid in the invitations database
54
     * returns a string with the following values:
55
     * 
56
     * OK-NEW valid token exists, and is not attached to an existing institution. When consuming the token, a new inst will be created
57
     * OK-EXISTING valid token exists, and is attached to an existing institution. When consuming the token, user will be added as an admin
58
     * FAIL-NONEXISTINGTOKEN this token does not exist at all in the database
59
     * FAIL-ALREADYCONSUMED the token exists, but has been used before
60
     * FAIL-EXPIRED the token exists, but has expired
61
     * 
62
     * @param string $token
63
     * @return string
64
     */
65
    public function checkTokenValidity($token) {
66
        $escapedToken = DBConnection::escape_value(UserManagement::$DB_TYPE, $token);
67
        $check = DBConnection::exec(UserManagement::$DB_TYPE, "SELECT invite_token, cat_institution_id 
68
                           FROM invitations 
69
                           WHERE invite_token = '$escapedToken' AND invite_created >= TIMESTAMPADD(DAY, -1, NOW()) AND used = 0");
70
71
        if ($tokenCheck = mysqli_fetch_object($check)) {
72
            if ($tokenCheck->cat_institution_id === NULL) {
73
                return "OK-NEW";
74
            }
75
            return "OK-EXISTING";
76
        }
77
        // if we haven't returned from the function yet, it is an invalid token... 
78
        // be a little verbose what's wrong with it
79
        $check_reason = DBConnection::exec(UserManagement::$DB_TYPE, "SELECT invite_token, used FROM invitations WHERE invite_token = '$escapedToken'");
80
        if ($invalidTokenCheck = mysqli_fetch_object($check_reason)) {
81
            if ($invalidTokenCheck->used == 1) {
82
                return "FAIL-ALREADYCONSUMED";
83
            }
84
            return "FAIL-EXPIRED";
85
        }
86
        return "FAIL-NONEXISTINGTOKEN";
87
    }
88
89
    /**
90
     * This function creates a new IdP in the database based on a valid invitation token - or adds a new administrator
91
     * to an existing one. The institution is created for the logged-in user (second argument) who presents the token (first 
92
     * argument). The tokens are created via createToken().
93
     * 
94
     * @param string $token The invitation token (must exist in the database and be valid). 
95
     * @param string $owner Persistent User ID who becomes the administrator of the institution
96
     * @return IdP 
97
     */
98
    public function createIdPFromToken($token, $owner) {
99
        $escapedToken = DBConnection::escape_value(UserManagement::$DB_TYPE, $token);
100
        $escapedOwner = DBConnection::escape_value(UserManagement::$DB_TYPE, $owner);
101
        // the token either has cat_institution_id set -> new admin for existing inst
102
        // or contains a number of parameters from external DB -> set up new inst
103
        $instinfo = DBConnection::exec(UserManagement::$DB_TYPE, "SELECT cat_institution_id, country, name, invite_issuer_level, invite_dest_mail, external_db_uniquehandle 
104
                             FROM invitations 
105
                             WHERE invite_token = '$escapedToken' AND invite_created >= TIMESTAMPADD(DAY, -1, NOW()) AND used = 0");
106
        if ($invitationDetails = mysqli_fetch_object($instinfo)) {
107
            if ($invitationDetails->cat_institution_id !== NULL) { // add new admin to existing inst
108
                DBConnection::exec(UserManagement::$DB_TYPE, "INSERT INTO ownership (user_id, institution_id, blesslevel, orig_mail) VALUES('$escapedOwner', $invitationDetails->cat_institution_id, '$invitationDetails->invite_issuer_level', '$invitationDetails->invite_dest_mail') ON DUPLICATE KEY UPDATE blesslevel='$invitationDetails->invite_issuer_level', orig_mail='$invitationDetails->invite_dest_mail' ");
109
                CAT::writeAudit($escapedOwner, "OWN", "IdP " . $invitationDetails->cat_institution_id . " - added user as owner");
110
                return new IdP($invitationDetails->cat_institution_id);
111
            } else { // create new IdP
112
                $fed = new Federation($invitationDetails->country);
113
                $idp = new IdP($fed->newIdP($escapedOwner, $invitationDetails->invite_issuer_level, $invitationDetails->invite_dest_mail));
114
115
                if ($invitationDetails->external_db_uniquehandle != NULL) {
116
                    $idp->setExternalDBId($invitationDetails->external_db_uniquehandle);
117
                    $externalinfo = Federation::getExternalDBEntityDetails($invitationDetails->external_db_uniquehandle);
118
                    foreach ($externalinfo['names'] as $instlang => $instname) {
119
                        $idp->addAttribute("general:instname", serialize(['lang' => $instlang, 'content' => $instname]));
120
                    }
121
                    // see if we had a C language, and if not, pick a good candidate
122
                    if (!array_key_exists('C', $externalinfo['names'])) {
123
                        if (array_key_exists('en', $externalinfo['names'])) { // English is a good candidate
124
                            $idp->addAttribute("general:instname", serialize(['lang' => 'C', 'content' => $externalinfo['names']['en']]));
125
                            $bestnameguess = $externalinfo['names']['en'];
126
                        } else { // no idea, let's take the first language we found
127
                            $idp->addAttribute("general:instname", serialize(['lang' => 'C', 'content' => reset($externalinfo['names'])]));
128
                            $bestnameguess = reset($externalinfo['names']);
129
                        }
130
                    }
131
                } else {
132
                    $idp->addAttribute("general:instname", serialize(['lang' => 'C', 'content' => $invitationDetails->name]));
133
                    $bestnameguess = $invitationDetails->name;
134
                }
135
                CAT::writeAudit($escapedOwner, "NEW", "IdP " . $idp->identifier . " - created from invitation");
136
137
                $admins = $fed->listFederationAdmins();
138
139
                // notify the fed admins...
140
141
                foreach ($admins as $id) {
142
                    $user = new User($id);
143
                    /// arguments are: 1. IdP name; 
144
                    ///                2. consortium name (e.g. eduroam); 
145
                    ///                3. federation shortname, e.g. "LU"; 
146
                    ///                4. product name (e.g. eduroam CAT); 
147
                    ///                5. product long name (e.g. eduroam Configuration Assistant Tool)
148
                    $message = sprintf(_("Hi,
149
150
the invitation for the new Identity Provider %s in your %s federation %s has been used and the IdP was created in %s.
151
152
We thought you might want to know.
153
154
Best regards,
155
156
%s"), $bestnameguess, Config::$CONSORTIUM['name'], strtoupper($fed->name), Config::$APPEARANCE['productname'], Config::$APPEARANCE['productname_long']);
0 ignored issues
show
Bug introduced by
The variable $bestnameguess does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
157
                    $retval = $user->sendMailToUser(_("IdP in your federation was created"), $message);
158
                    if ($retval == FALSE) {
159
                        debug(2, "Mail to federation admin was NOT sent!\n");
160
                    }
161
                }
162
163
                return $idp;
164
            }
165
        }
166
    }
167
168
    /**
169
     * Adds a new administrator to an existing IdP
170
     * @param IdP $idp institution to which the admin is to be added.
171
     * @param string $user persistent user ID that is to be added as an admin.
172
     * @return boolean This function always returns TRUE.
173
     */
174
    public function addAdminToIdp($idp, $user) {
175
        $escapedUser = DBConnection::escape_value(UserManagement::$DB_TYPE, $user);
176
        DBConnection::exec(UserManagement::$DB_TYPE, "INSERT IGNORE into ownership (institution_id,user_id,blesslevel,orig_mail) VALUES($idp->identifier,'$escapedUser','FED','SELF-APPOINTED')");
177
        return TRUE;
178
    }
179
180
    /**
181
     * Deletes an administrator from the IdP. If the IdP and user combination doesn't match, nothing happens.
182
     * @param IdP $idp institution from which the admin is to be deleted.
183
     * @param string $user persistent user ID that is to be deleted as an admin.
184
     * @return boolean This function always returns TRUE.
185
     */
186
    public function removeAdminFromIdP($idp, $user) {
187
        $escapedUser = DBConnection::escape_value(UserManagement::$DB_TYPE, $user);
188
        DBConnection::exec(UserManagement::$DB_TYPE, "DELETE from ownership WHERE institution_id = $idp->identifier AND user_id = '$escapedUser'");
189
        return TRUE;
190
    }
191
192
    /**
193
     * Invalidates a token so that it can't be used any more. Tokens automatically expire after 24h, but can be invalidated
194
     * earlier, e.g. after having been used to create an institution. If the token doesn't exist in the DB or is already invalidated,
195
     * nothing happens.
196
     * 
197
     * @param string $token the token to invalidate
198
     * @return boolean This function always returns TRUE.
199
     */
200
    public function invalidateToken($token) {
201
        $escapedToken = DBConnection::escape_value(UserManagement::$DB_TYPE, $token);
202
        DBConnection::exec(UserManagement::$DB_TYPE, "UPDATE invitations SET used = 1 WHERE invite_token = '$escapedToken'");
203
        return TRUE;
204
    }
205
206
    /**
207
     * Creates a new invitation token. The token's main purpose is to be sent out by mail. The function either can generate a token for a new 
208
     * administrator of an existing institution, or for a new institution. In the latter case, the institution only actually gets 
209
     * created in the DB if the token is actually consumed via createIdPFromToken().
210
     * 
211
     * @param boolean $by_fedadmin is the invitation token created for a federation admin or from an existing inst admin
212
     * @param type $for identifier (typically email address) for which the invitation is created
213
     * @param mixed $inst_identifier either an instance of the IdP class (for existing institutions to invite new admins) or a string (new institution - this is the inst name then)
214
     * @param string $external_id if the IdP to be created is related to an external DB entity, this parameter contains that ID
215
     * @param type $country if the institution is new (i.e. $inst is a string) this parameter needs to specify the federation of the new inst
216
     * @return mixed The function returns either the token (as string) or FALSE if something went wrong
217
     */
218
    public function createToken($by_fedadmin, $for, $inst_identifier, $external_id = 0, $country = 0) {
219
        $escapedFor = DBConnection::escape_value(UserManagement::$DB_TYPE, $for);
0 ignored issues
show
Documentation introduced by
$for is of type object<Type>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
220
        $token = sha1(base_convert(rand(10e16, 10e20), 10, 36)) . sha1(base_convert(rand(10e16, 10e20), 10, 36));
221
        $level = ($by_fedadmin ? "FED" : "INST");
222
223
        if ($inst_identifier instanceof IdP) {
224
            DBConnection::exec(UserManagement::$DB_TYPE, "INSERT INTO invitations (invite_issuer_level, invite_dest_mail, invite_token,cat_institution_id) VALUES('$level', '$escapedFor', '$token',$inst_identifier->identifier)");
225
            return $token;
226
        } else if (func_num_args() == 4) { // string name, but no country - new IdP with link to external DB
227
            // what country are we talking about?
228
            $newname = DBConnection::escape_value(UserManagement::$DB_TYPE, valid_string_db($inst_identifier));
0 ignored issues
show
Bug introduced by
It seems like valid_string_db($inst_identifier) targeting valid_string_db() can also be of type array<integer,string>; however, DBConnection::escape_value() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
229
            $extinfo = Federation::getExternalDBEntityDetails($external_id);
230
            $externalhandle = DBConnection::escape_value(UserManagement::$DB_TYPE, valid_string_db($external_id));
0 ignored issues
show
Bug introduced by
It seems like valid_string_db($external_id) targeting valid_string_db() can also be of type array<integer,string>; however, DBConnection::escape_value() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
231
            DBConnection::exec(UserManagement::$DB_TYPE, "INSERT INTO invitations (invite_issuer_level, invite_dest_mail, invite_token,name,country, external_db_uniquehandle) VALUES('$level', '$escapedFor', '$token', '" . $newname . "', '" . $extinfo['country'] . "',  '" . $externalhandle . "')");
232
            return $token;
233
        } else if (func_num_args() == 5) { // string name, and country set - whole new IdP
234
            $newname = DBConnection::escape_value(UserManagement::$DB_TYPE, valid_string_db($inst_identifier));
0 ignored issues
show
Bug introduced by
It seems like valid_string_db($inst_identifier) targeting valid_string_db() can also be of type array<integer,string>; however, DBConnection::escape_value() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
235
            $newcountry = DBConnection::escape_value(UserManagement::$DB_TYPE, valid_string_db($country));
0 ignored issues
show
Bug introduced by
It seems like valid_string_db($country) targeting valid_string_db() can also be of type array<integer,string>; however, DBConnection::escape_value() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
236
            DBConnection::exec(UserManagement::$DB_TYPE, "INSERT INTO invitations (invite_issuer_level, invite_dest_mail, invite_token,name,country) VALUES('$level', '$escapedFor', '$token', '" . $newname . "', '" . $newcountry . "')");
237
            return $token;
238
        }
239
        echo "FAIL!";
240
        return FALSE;
241
    }
242
243
    /**
244
     * Retrieves all pending invitations for an institution or for a federation.
245
     * 
246
     * @param type $idp_identifier the identifier of the institution. If not set, returns invitations for not-yet-created insts
247
     * @return if idp_identifier is set: an array of strings (mail addresses); otherwise an array of tuples (country;name;mail)
248
     */
249
    public function listPendingInvitations($idp_identifier = 0) {
250
        $retval = [];
251
        $invitations = DBConnection::exec(UserManagement::$DB_TYPE, "SELECT cat_institution_id, country, name, invite_issuer_level, invite_dest_mail, invite_token 
252
                                        FROM invitations 
253
                                        WHERE cat_institution_id " . ( $idp_identifier != 0 ? "= $idp_identifier" : "IS NULL") . " AND invite_created >= TIMESTAMPADD(DAY, -1, NOW()) AND used = 0");
254
        if ($idp_identifier != 0) { // list invitations for existing institution, must match cat_institution_id
255
            while ($a = mysqli_fetch_object($invitations)) {
256
                debug(4, "Retrieving pending invitations for IdP $idp_identifier.\n");
257
                if ($a->cat_institution_id == $idp_identifier) {
258
                    $retval[] = $a->invite_dest_mail;
259
                }
260
            }
261
        } else { // list all invitations for *new* institutions
262
            while ($a = mysqli_fetch_object($invitations)) {
263
                debug(4, "Retrieving pending invitations for NEW institutions.\n");
264
                if ($a->cat_institution_id == NULL) {
265
                    $retval[] = ["country" => $a->country, "name" => $a->name, "mail" => $a->invite_dest_mail, "token" => $a->invite_token];
266
                }
267
            }
268
        }
269
        return $retval;
270
    }
271
272
    /** Retrieves all invitations which have expired in the last hour.
273
     * 
274
     * @return array of expired invitations
275
     */
276
    public function listRecentlyExpiredInvitations() {
277
        $retval = [];
278
        $invitations = DBConnection::exec(UserManagement::$DB_TYPE, "SELECT cat_institution_id, country, name, invite_issuer_level, invite_dest_mail, invite_token 
279
                                        FROM invitations 
280
                                        WHERE invite_created >= TIMESTAMPADD(HOUR, -25, NOW()) AND invite_created < TIMESTAMPADD(HOUR, -24, NOW()) AND used = 0");
281
        while ($a = mysqli_fetch_object($invitations)) {
282
            debug(4, "Retrieving recently expired invitations (expired in last hour)\n");
283
            if ($a->cat_institution_id == NULL) {
284
                $retval[] = ["country" => $a->country, "level" => $a->invite_issuer_level, "name" => $a->name, "mail" => $a->invite_dest_mail];
285
            } else {
286
                $retval[] = ["country" => $a->country, "level" => $a->invite_issuer_level, "name" => "Existing IdP", "mail" => $a->invite_dest_mail];
287
            }
288
        }
289
        return $retval;
290
    }
291
292
    /**
293
     * For a given persistent user identifier, returns an array of institution identifiers (not the actual objects!) for which this
294
     * user is the/a administrator.
295
     * 
296
     * @param string $userid persistent user identifier
297
     * @return array array of institution IDs
298
     */
299
    public function listInstitutionsByAdmin($userid) {
300
        $returnarray = [];
301
        $escapedUserid = DBConnection::escape_value(UserManagement::$DB_TYPE, $userid);
302
        $institutions = DBConnection::exec(UserManagement::$DB_TYPE, "SELECT institution_id FROM ownership WHERE user_id = '$escapedUserid' ORDER BY institution_id");
303
        while ($a = mysqli_fetch_object($institutions)) {
304
            $returnarray[] = $a->institution_id;
305
        }
306
        return $returnarray;
307
    }
308
}
309