|
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 |
|
|
|
|
|
|
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... |
|
|
|
|
|
|
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... |
|
|
|
|
|
|
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
|
|
|
|