Test Failed
Push — master ( 0071b0...4282d1 )
by Stefan
05:33
created

DeploymentManaged::__construct()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 34
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 28
dl 0
loc 34
c 0
b 0
f 0
rs 8.8497
cc 6
nc 7
nop 2
1
<?php
2
3
/*
4
 * *****************************************************************************
5
 * Contributions to this work were made on behalf of the GÉANT project, a 
6
 * project that has received funding from the European Union’s Framework 
7
 * Programme 7 under Grant Agreements No. 238875 (GN3) and No. 605243 (GN3plus),
8
 * Horizon 2020 research and innovation programme under Grant Agreements No. 
9
 * 691567 (GN4-1) and No. 731122 (GN4-2).
10
 * On behalf of the aforementioned projects, GEANT Association is the sole owner
11
 * of the copyright in all material which was developed by a member of the GÉANT
12
 * project. GÉANT Vereniging (Association) is registered with the Chamber of 
13
 * Commerce in Amsterdam with registration number 40535155 and operates in the 
14
 * UK as a branch of GÉANT Vereniging.
15
 * 
16
 * Registered office: Hoekenrode 3, 1102BR Amsterdam, The Netherlands. 
17
 * UK branch address: City House, 126-130 Hills Road, Cambridge CB2 1PQ, UK
18
 *
19
 * License: see the web/copyright.inc.php file in the file structure or
20
 *          <base_url>/copyright.php after deploying the software
21
 */
22
23
/**
24
 * This file contains the AbstractProfile class. It contains common methods for
25
 * both RADIUS/EAP profiles and SilverBullet profiles
26
 *
27
 * @author Stefan Winter <[email protected]>
28
 * @author Tomasz Wolniewicz <[email protected]>
29
 *
30
 * @package Developer
31
 *
32
 */
33
34
namespace core;
35
36
use \Exception;
37
38
/**
39
 * This class represents an EAP Profile.
40
 * Profiles can inherit attributes from their IdP, if the IdP has some. Otherwise,
41
 * one can set attribute in the Profile directly. If there is a conflict between
42
 * IdP-wide and Profile-wide attributes, the more specific ones (i.e. Profile) win.
43
 * 
44
 * @author Stefan Winter <[email protected]>
45
 * @author Tomasz Wolniewicz <[email protected]>
46
 *
47
 * @license see LICENSE file in root directory
48
 *
49
 * @package Developer
50
 */
51
class DeploymentManaged extends AbstractDeployment {
52
53
    const MAX_CLIENTS_PER_SERVER = 1000;
54
55
    const PRODUCTNAME = "Managed SP";
56
    /**
57
     * the RADIUS port for this SP instance
58
     * 
59
     * @var int
60
     */
61
    public $port;
62
63
    /**
64
     * the shared secret for this SP instance
65
     * 
66
     * @var string
67
     */
68
    public $secret;
69
70
    /**
71
     * the IPv4 address of the RADIUS server for this SP instance (can be NULL)
72
     * 
73
     * @var string
74
     */
75
    public $host4;
76
    
77
    /**
78
     * the IPv6 address of the RADIUS server for this SP instance (can be NULL)
79
     * 
80
     * @var string
81
     */
82
    public $host6;
83
    /**
84
     * the RADIUS server instance for this SP instance
85
     * 
86
     * @var string
87
     */
88
    public $radius_instance;
89
90
    /**
91
     * Class constructor for existing deployments (use 
92
     * IdP::newDeployment() to actually create one). Retrieves all 
93
     * attributes from the DB and stores them in the priv_ arrays.
94
     * 
95
     * @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.
96
     * @param string|int $deploymentIdRaw identifier of the deployment in the DB
97
     */
98
    public function __construct($idpObject, $deploymentIdRaw) {
99
        parent::__construct($idpObject, $deploymentIdRaw); // we now have access to our INST database handle and logging
100
        $this->entityOptionTable = "deployment_option";
101
        $this->entityIdColumn = "deployment_id";
102
        $this->type = AbstractDeployment::DEPLOYMENTTYPE_MANAGED;
103
        if (!is_numeric($deploymentIdRaw)) {
104
            throw new Exception("Managed SP instances have to have a numeric identifier");
105
        }
106
        $propertyQuery = "SELECT status,port,secret,radius_instance FROM deployment WHERE deployment_id = ?";
107
        $queryExec = $this->databaseHandle->exec($propertyQuery, "i", $deploymentIdRaw);
108
        while ($iterator = mysqli_fetch_object(/** @scrutinizer ignore-type */ $queryExec)) {
109
            if ($iterator->secret == NULL && $iterator->radius_instance == NULL) {
110
                // we are instantiated for the first time, initialise us
111
                $details = $this->initialise();
112
                $this->port = $details["port"];
113
                $this->secret = $details["secret"];
114
                $this->radius_instance = $details["radius_instance"];
115
                $this->status = AbstractDeployment::INACTIVE;
116
            } else {
117
                $this->port = $iterator->port;
118
                $this->secret = $iterator->secret;
119
                $this->radius_instance = $iterator->radius_instance;
120
                $this->status = $iterator->status;
121
            }
122
        }
123
        $serverdetails = $this->databaseHandle->exec("SELECT radius_ip4, radius_ip6 FROM managed_sp_servers WHERE server_id = '$this->radius_instance'");
124
        while ($iterator2 = mysqli_fetch_object(/** @scrutinizer ignore-type */ $serverdetails)) {
125
            $this->host4 = $iterator2->radius_ip4;
126
            $this->host6 = $iterator2->radius_ip6;
127
        }
128
        $this->attributes = $this->retrieveOptionsFromDatabase("SELECT DISTINCT option_name, option_lang, option_value, row 
129
                                            FROM $this->entityOptionTable
130
                                            WHERE $this->entityIdColumn = ?  
131
                                            ORDER BY option_name", "Profile");
132
    }
133
134
    /**
135
     * initialises a new SP
136
     * 
137
     * @return array details of the SP as generated during initialisation
138
     * @throws Exception
139
     */
140
    private function initialise() {
141
        // find a server near us (list of all servers, ordered by distance)
142
        $servers = $this->databaseHandle->exec("SELECT server_id, location_lon, location_lat FROM managed_sp_servers");
143
        $ourserver = [];
144
        // TODO: for ease of prototyping, no particular order - add location-based selection later
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
145
        while ($iterator = mysqli_fetch_object(/** @scrutinizer ignore-type */ $servers)) {
146
            $clientCount = $this->databaseHandle->exec("SELECT count(port) AS tenants FROM deployment WHERE radius_instance = '$iterator->server_id'");
147
            while ($iterator2 = mysqli_fetch_object(/** @scrutinizer ignore-type */ $clientCount)) {
148
                $clients = $iterator2->tenants;
149
                if ($clients < DeploymentManaged::MAX_CLIENTS_PER_SERVER) {
150
                    $ourserver[] = $iterator->server_id;
151
                }
152
                if ($clients > DeploymentManaged::MAX_CLIENTS_PER_SERVER * 0.9) {
153
                    $this->loggerInstance->debug(1, "A RADIUS server for Managed SP (" . $iterator->server_id . ") is serving at more than 90% capacity!");
154
                }
155
            }
156
        }
157
        if (count($ourserver) == 0) {
158
            throw new Exception("No available server found for new SP!");
159
        }
160
        // now, find an unused port in our preferred server
161
        $foundFreePort = 0;
162
        while ($foundFreePort == 0) {
163
            $portCandidate = random_int(1025, 65535);
164
            $check = $this->databaseHandle->exec("SELECT port FROM deployment WHERE radius_instance = '" . $ourserver[0] . "' AND port = $portCandidate");
165
            if (mysqli_num_rows(/** @scrutinizer ignore-type */ $check) == 0) {
166
                $foundFreePort = $portCandidate;
167
            }
168
        };
169
        // and make up a shared secret that is halfways readable
170
        $futureSecret = $this->randomString(16, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ");
171
        $this->databaseHandle->exec("UPDATE deployment SET radius_instance = '".$ourserver[0]."', port = $foundFreePort, secret = '$futureSecret' WHERE deployment_id = $this->identifier");
172
        return ["port" => $foundFreePort, "secret" => $futureSecret, "radius_instance" => $ourserver[0]];
173
    }
174
175
    /**
176
     * update the last_changed timestamp for this deployment
177
     * 
178
     * @return void
179
     */
180
    public function updateFreshness() {
181
        $this->databaseHandle->exec("UPDATE deployment SET last_change = CURRENT_TIMESTAMP WHERE deployment_id = $this->identifier");
182
    }
183
184
    /**
185
     * gets the last-modified timestamp (useful for caching "dirty" check)
186
     * 
187
     * @return string the date in string form, as returned by SQL
188
     */
189
    public function getFreshness() {
190
        $execLastChange = $this->databaseHandle->exec("SELECT last_change FROM deployment WHERE deployment_id = $this->identifier");
191
        // SELECT always returns a resource, never a boolean
192
        if ($freshnessQuery = mysqli_fetch_object(/** @scrutinizer ignore-type */ $execLastChange)) {
193
            return $freshnessQuery->last_change;
194
        }
195
    }
196
197
    /**
198
     * Deletes the deployment from database
199
     * 
200
     * @return void
201
     */
202
    public function destroy() {
203
        $this->databaseHandle->exec("DELETE FROM deployment_option WHERE deployment_id = $this->identifier");
204
        $this->databaseHandle->exec("DELETE FROM deployment WHERE deployment_id = $this->identifier");
205
    }
206
207
    /**
208
     * deactivates the deployment.
209
     * TODO: needs to call the RADIUS server reconfiguration routines...
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
210
     * 
211
     * @return void
212
     */
213
    public function deactivate() {
214
        $this->databaseHandle->exec("UPDATE deployment SET status = ".DeploymentManaged::INACTIVE." WHERE deployment_id = $this->identifier");
215
    }
216
    
217
    /**
218
     * activates the deployment.
219
     * TODO: needs to call the RADIUS server reconfiguration routines...
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
220
     * 
221
     * @return void
222
     */
223
    public function activate() {
224
        $this->databaseHandle->exec("UPDATE deployment SET status = ".DeploymentManaged::ACTIVE." WHERE deployment_id = $this->identifier");
225
    }
226
}
227