1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* ClientService.php |
4
|
|
|
* |
5
|
|
|
* PHP version 5.6+ |
6
|
|
|
* |
7
|
|
|
* @author Philippe Gaultier <[email protected]> |
8
|
|
|
* @copyright 2010-2017 Philippe Gaultier |
9
|
|
|
* @license http://www.sweelix.net/license license |
10
|
|
|
* @version 1.2.0 |
11
|
|
|
* @link http://www.sweelix.net |
12
|
|
|
* @package sweelix\oauth2\server\services\redis |
13
|
|
|
*/ |
14
|
|
|
|
15
|
|
|
namespace sweelix\oauth2\server\services\redis; |
16
|
|
|
|
17
|
|
|
use sweelix\oauth2\server\exceptions\DuplicateIndexException; |
18
|
|
|
use sweelix\oauth2\server\exceptions\DuplicateKeyException; |
19
|
|
|
use sweelix\oauth2\server\interfaces\ClientModelInterface; |
20
|
|
|
use sweelix\oauth2\server\interfaces\ClientServiceInterface; |
21
|
|
|
use yii\db\Exception as DatabaseException; |
22
|
|
|
use Yii; |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* This is the client service for redis |
26
|
|
|
* database structure |
27
|
|
|
* * oauth2:clients:<cid> : hash (Client) |
28
|
|
|
* * oauth2:clients:<cid>:users : set |
29
|
|
|
* * oauth2:users:<uid>:clients : set |
30
|
|
|
* |
31
|
|
|
* @author Philippe Gaultier <[email protected]> |
32
|
|
|
* @copyright 2010-2017 Philippe Gaultier |
33
|
|
|
* @license http://www.sweelix.net/license license |
34
|
|
|
* @version 1.2.0 |
35
|
|
|
* @link http://www.sweelix.net |
36
|
|
|
* @package sweelix\oauth2\server\services\redis |
37
|
|
|
* @since 1.0.0 |
38
|
|
|
*/ |
39
|
|
|
class ClientService extends BaseService implements ClientServiceInterface |
40
|
|
|
{ |
41
|
|
|
/** |
42
|
|
|
* @var string user namespace (collection for clients) |
43
|
|
|
*/ |
44
|
|
|
public $userNamespace = ''; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* @param string $cid client ID |
48
|
|
|
* @return string client Key |
49
|
|
|
* @since 1.0.0 |
50
|
|
|
*/ |
51
|
|
|
public function getClientKey($cid) |
52
|
25 |
|
{ |
53
|
|
|
return $this->namespace . ':' . $cid; |
54
|
25 |
|
} |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* @param string $cid client ID |
58
|
|
|
* @return string clientUsers Key |
59
|
|
|
* @since 1.0.0 |
60
|
|
|
*/ |
61
|
|
|
public function getClientUsersListKey($cid) |
62
|
6 |
|
{ |
63
|
|
|
return $this->namespace . ':' . $cid . ':users'; |
64
|
6 |
|
} |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* @param string $uid user ID |
68
|
|
|
* @return string user clients collection Key |
69
|
|
|
* @since XXX |
70
|
|
|
*/ |
71
|
|
|
public function getUserClientsListKey($uid) |
72
|
5 |
|
{ |
73
|
|
|
return $this->userNamespace . ':' . $uid . ':clients'; |
74
|
5 |
|
} |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* @return string key of all clients list |
78
|
|
|
*/ |
79
|
|
|
public function getClientListKey() |
80
|
21 |
|
{ |
81
|
|
|
return $this->namespace . ':keys'; |
82
|
21 |
|
} |
83
|
21 |
|
|
84
|
21 |
|
/** |
85
|
2 |
|
* @inheritdoc |
86
|
|
|
*/ |
87
|
21 |
|
public function save(ClientModelInterface $client, $attributes) |
88
|
|
|
{ |
89
|
|
|
if ($client->getIsNewRecord()) { |
90
|
|
|
$result = $this->insert($client, $attributes); |
91
|
|
|
} else { |
92
|
|
|
$result = $this->update($client, $attributes); |
93
|
|
|
} |
94
|
|
|
return $result; |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* Save Client |
99
|
|
|
* @param ClientModelInterface $client |
100
|
21 |
|
* @param null|array $attributes attributes to save |
101
|
|
|
* @return bool |
102
|
21 |
|
* @throws DatabaseException |
103
|
21 |
|
* @throws DuplicateIndexException |
104
|
|
|
* @throws DuplicateKeyException |
105
|
|
|
* @since 1.0.0 |
106
|
21 |
|
*/ |
107
|
|
|
protected function insert(ClientModelInterface $client, $attributes) |
108
|
21 |
|
{ |
109
|
21 |
|
$result = false; |
110
|
1 |
|
if (!$client->beforeSave(true)) { |
111
|
|
|
return $result; |
112
|
|
|
} |
113
|
21 |
|
$clientKey = $this->getClientKey($client->getKey()); |
114
|
21 |
|
$clientListKey = $this->getClientListKey(); |
115
|
21 |
|
//check if record exists |
116
|
21 |
|
$entityStatus = (int)$this->db->executeCommand('EXISTS', [$clientKey]); |
117
|
|
|
if ($entityStatus === 1) { |
118
|
21 |
|
throw new DuplicateKeyException('Duplicate key "' . $clientKey . '"'); |
119
|
21 |
|
} |
120
|
21 |
|
|
121
|
21 |
|
$values = $client->getDirtyAttributes($attributes); |
|
|
|
|
122
|
21 |
|
$redisParameters = [$clientKey]; |
123
|
|
|
$this->setAttributesDefinitions($client->attributesDefinition()); |
124
|
21 |
|
foreach ($values as $key => $value) { |
125
|
21 |
|
if ($value !== null) { |
126
|
|
|
$redisParameters[] = $key; |
127
|
21 |
|
$redisParameters[] = $this->convertToDatabase($key, $value); |
128
|
21 |
|
} |
129
|
21 |
|
} |
130
|
|
|
//TODO: use EXEC/MULTI to avoid errors |
131
|
|
|
$transaction = $this->db->executeCommand('MULTI'); |
132
|
|
|
if ($transaction === true) { |
133
|
|
|
try { |
134
|
|
|
$this->db->executeCommand('HMSET', $redisParameters); |
135
|
|
|
$this->db->executeCommand('SADD', [$clientListKey, $client->getKey()]); |
136
|
21 |
|
$this->db->executeCommand('EXEC'); |
137
|
21 |
|
} catch (DatabaseException $e) { |
138
|
21 |
|
// @codeCoverageIgnoreStart |
139
|
21 |
|
// we have a REDIS exception, we should not discard |
140
|
21 |
|
Yii::debug('Error while inserting entity', __METHOD__); |
141
|
21 |
|
throw $e; |
142
|
|
|
// @codeCoverageIgnoreEnd |
143
|
|
|
} |
144
|
|
|
} |
145
|
|
|
$changedAttributes = array_fill_keys(array_keys($values), null); |
146
|
|
|
$client->setOldAttributes($values); |
147
|
|
|
$client->afterSave(true, $changedAttributes); |
148
|
|
|
$result = true; |
149
|
|
|
return $result; |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
|
153
|
|
|
/** |
154
|
2 |
|
* Update Client |
155
|
|
|
* @param ClientModelInterface $client |
156
|
2 |
|
* @param null|array $attributes attributes to save |
157
|
|
|
* @return bool |
158
|
|
|
* @throws DatabaseException |
159
|
|
|
* @throws DuplicateIndexException |
160
|
2 |
|
* @throws DuplicateKeyException |
161
|
2 |
|
*/ |
162
|
2 |
|
protected function update(ClientModelInterface $client, $attributes) |
163
|
2 |
|
{ |
164
|
|
|
if (!$client->beforeSave(false)) { |
165
|
|
|
return false; |
166
|
2 |
|
} |
167
|
1 |
|
|
168
|
1 |
|
$values = $client->getDirtyAttributes($attributes); |
|
|
|
|
169
|
1 |
|
$modelKey = $client->key(); |
170
|
1 |
|
$clientId = isset($values[$modelKey]) ? $values[$modelKey] : $client->getKey(); |
171
|
|
|
$clientKey = $this->getClientKey($clientId); |
172
|
1 |
|
$clientListKey = $this->getClientListKey(); |
173
|
|
|
|
174
|
2 |
|
if (isset($values[$modelKey]) === true) { |
175
|
|
|
$newClientKey = $this->getClientKey($values[$modelKey]); |
176
|
2 |
|
$entityStatus = (int)$this->db->executeCommand('EXISTS', [$newClientKey]); |
177
|
1 |
|
if ($entityStatus === 1) { |
178
|
1 |
|
throw new DuplicateKeyException('Duplicate key "' . $newClientKey . '"'); |
179
|
|
|
} |
180
|
1 |
|
} |
181
|
1 |
|
|
182
|
|
|
$this->db->executeCommand('MULTI'); |
183
|
2 |
|
try { |
184
|
2 |
|
$reAddKeyInList = false; |
185
|
2 |
|
if (array_key_exists($modelKey, $values) === true) { |
186
|
2 |
|
$oldId = $client->getOldKey(); |
187
|
|
|
$oldClientKey = $this->getClientKey($oldId); |
188
|
2 |
|
|
189
|
1 |
|
$this->db->executeCommand('RENAMENX', [$oldClientKey, $clientKey]); |
190
|
1 |
|
$this->db->executeCommand('SREM', [$clientListKey, $oldClientKey]); |
191
|
2 |
|
$reAddKeyInList = true; |
192
|
2 |
|
} |
193
|
|
|
|
194
|
2 |
|
$redisUpdateParameters = [$clientKey]; |
195
|
2 |
|
$redisDeleteParameters = [$clientKey]; |
196
|
1 |
|
$this->setAttributesDefinitions($client->attributesDefinition()); |
197
|
1 |
|
foreach ($values as $key => $value) { |
198
|
2 |
|
if ($value === null) { |
199
|
2 |
|
$redisDeleteParameters[] = $key; |
200
|
2 |
|
} else { |
201
|
|
|
$redisUpdateParameters[] = $key; |
202
|
2 |
|
$redisUpdateParameters[] = $this->convertToDatabase($key, $value); |
203
|
2 |
|
} |
204
|
|
|
} |
205
|
|
|
if (count($redisDeleteParameters) > 1) { |
206
|
|
|
$this->db->executeCommand('HDEL', $redisDeleteParameters); |
207
|
|
|
} |
208
|
|
|
if (count($redisUpdateParameters) > 1) { |
209
|
|
|
$this->db->executeCommand('HMSET', $redisUpdateParameters); |
210
|
|
|
} |
211
|
2 |
|
|
212
|
2 |
|
if ($reAddKeyInList === true) { |
213
|
2 |
|
$this->db->executeCommand('SADD', [$clientListKey, $clientId]); |
214
|
2 |
|
} |
215
|
2 |
|
|
216
|
2 |
|
$this->db->executeCommand('EXEC'); |
217
|
2 |
|
} catch (DatabaseException $e) { |
218
|
2 |
|
// @codeCoverageIgnoreStart |
219
|
|
|
// we have a REDIS exception, we should not discard |
220
|
|
|
Yii::debug('Error while updating entity', __METHOD__); |
221
|
|
|
throw $e; |
222
|
|
|
// @codeCoverageIgnoreEnd |
223
|
|
|
} |
224
|
23 |
|
|
225
|
|
|
$changedAttributes = []; |
226
|
23 |
|
foreach ($values as $name => $value) { |
227
|
23 |
|
$oldAttributes = $client->getOldAttributes(); |
228
|
23 |
|
$changedAttributes[$name] = isset($oldAttributes[$name]) ? $oldAttributes[$name] : null; |
229
|
23 |
|
$client->setOldAttribute($name, $value); |
230
|
18 |
|
} |
231
|
18 |
|
$client->afterSave(false, $changedAttributes); |
232
|
|
|
return true; |
233
|
18 |
|
} |
234
|
18 |
|
|
235
|
18 |
|
/** |
236
|
18 |
|
* @inheritdoc |
237
|
18 |
|
*/ |
238
|
18 |
|
public function findOne($key) |
239
|
18 |
|
{ |
240
|
18 |
|
$record = null; |
241
|
|
|
$clientKey = $this->getClientKey($key); |
242
|
|
|
$clientExists = (bool)$this->db->executeCommand('EXISTS', [$clientKey]); |
243
|
|
|
if ($clientExists === true) { |
244
|
|
|
$clientData = $this->db->executeCommand('HGETALL', [$clientKey]); |
245
|
|
|
$record = Yii::createObject('sweelix\oauth2\server\interfaces\ClientModelInterface'); |
246
|
|
|
/** @var ClientModelInterface $record */ |
247
|
18 |
|
$properties = $record->attributesDefinition(); |
248
|
18 |
|
$this->setAttributesDefinitions($properties); |
249
|
18 |
|
$attributes = []; |
250
|
18 |
|
for ($i = 0; $i < count($clientData); $i += 2) { |
|
|
|
|
251
|
18 |
|
if (isset($properties[$clientData[$i]]) === true) { |
252
|
18 |
|
$clientData[$i + 1] = $this->convertToModel($clientData[$i], $clientData[($i + 1)]); |
253
|
23 |
|
$record->setAttribute($clientData[$i], $clientData[$i + 1]); |
254
|
|
|
$attributes[$clientData[$i]] = $clientData[$i + 1]; |
255
|
|
|
// @codeCoverageIgnoreStart |
256
|
|
|
} elseif ($record->canSetProperty($clientData[$i])) { |
257
|
|
|
// TODO: find a way to test attribute population |
258
|
|
|
$record->{$clientData[$i]} = $clientData[$i + 1]; |
259
|
1 |
|
} |
260
|
|
|
// @codeCoverageIgnoreEnd |
261
|
1 |
|
} |
262
|
1 |
|
if (empty($attributes) === false) { |
263
|
1 |
|
$record->setOldAttributes($attributes); |
264
|
1 |
|
} |
265
|
1 |
|
$record->afterFind(); |
266
|
|
|
} |
267
|
|
|
return $record; |
268
|
|
|
} |
269
|
1 |
|
|
270
|
1 |
|
/** |
271
|
1 |
|
* @inheritdoc |
272
|
|
|
*/ |
273
|
1 |
|
public function delete(ClientModelInterface $client) |
274
|
1 |
|
{ |
275
|
1 |
|
$result = false; |
276
|
|
|
if ($client->beforeDelete()) { |
277
|
1 |
|
$id = $client->getOldKey(); |
278
|
|
|
$clientKey = $this->getClientKey($id); |
279
|
1 |
|
$clientListKey = $this->getClientListKey(); |
280
|
|
|
$clientUsersListKey = $this->getClientUsersListKey($id); |
281
|
1 |
|
|
282
|
|
|
// before cleaning the client, drop all access tokens, refresh tokens, auth codes and jtis |
283
|
|
|
$token = Yii::createObject('sweelix\oauth2\server\interfaces\AccessTokenModelInterface'); |
284
|
1 |
|
$tokenClass = get_class($token); |
285
|
1 |
|
$tokenClass::deleteAllByClientId($id); |
286
|
1 |
|
|
287
|
|
|
$token = Yii::createObject('sweelix\oauth2\server\interfaces\RefreshTokenModelInterface'); |
288
|
1 |
|
$tokenClass = get_class($token); |
289
|
1 |
|
$tokenClass::deleteAllByClientId($id); |
290
|
1 |
|
|
291
|
1 |
|
$token = Yii::createObject('sweelix\oauth2\server\interfaces\JtiModelInterface'); |
292
|
1 |
|
$tokenClass = get_class($token); |
293
|
1 |
|
$tokenClass::deleteAllByClientId($id); |
294
|
|
|
|
295
|
|
|
$usersList = $this->db->executeCommand('SMEMBERS', [$clientUsersListKey]); |
296
|
|
|
|
297
|
|
|
$this->db->executeCommand('MULTI'); |
298
|
|
|
// remove client from all userClient sets |
299
|
5 |
|
foreach ($usersList as $user) { |
300
|
|
|
$userClientKey = $this->getUserClientsListKey($user); |
301
|
5 |
|
$this->db->executeCommand('SREM', [$userClientKey, $id]); |
302
|
5 |
|
} |
303
|
5 |
|
$this->db->executeCommand('DEL', [$clientKey]); |
304
|
|
|
$this->db->executeCommand('DEL', [$clientUsersListKey]); |
305
|
|
|
$this->db->executeCommand('SREM', [$clientListKey, $id]); |
306
|
|
|
//TODO: check results to return correct information |
307
|
|
|
$queryResult = $this->db->executeCommand('EXEC'); |
|
|
|
|
308
|
|
|
$client->setIsNewRecord(true); |
309
|
3 |
|
$client->afterDelete(); |
310
|
|
|
$result = true; |
311
|
3 |
|
} |
312
|
3 |
|
return $result; |
313
|
3 |
|
} |
314
|
3 |
|
|
315
|
3 |
|
/** |
316
|
3 |
|
* @inheritdoc |
317
|
3 |
|
*/ |
318
|
|
|
public function hasUser(ClientModelInterface $client, $userId) |
319
|
3 |
|
{ |
320
|
|
|
$key = $client->getKey(); |
321
|
|
|
$clientUsersListKey = $this->getClientUsersListKey($key); |
322
|
|
|
return (bool)$this->db->executeCommand('SISMEMBER', [$clientUsersListKey, $userId]); |
323
|
|
|
} |
324
|
|
|
|
325
|
4 |
|
/** |
326
|
|
|
* @inheritdoc |
327
|
4 |
|
*/ |
328
|
4 |
|
public function addUser(ClientModelInterface $client, $userId) |
329
|
4 |
|
{ |
330
|
4 |
|
$key = $client->getKey(); |
331
|
4 |
|
$clientUsersListKey = $this->getClientUsersListKey($key); |
332
|
4 |
|
$userClientsListKey = $this->getUserClientsListKey($userId); |
333
|
4 |
|
$this->db->executeCommand('MULTI'); |
334
|
|
|
$this->db->executeCommand('SADD', [$clientUsersListKey, $userId]); |
335
|
4 |
|
$this->db->executeCommand('SADD', [$userClientsListKey, $key]); |
336
|
|
|
$queryResult = $this->db->executeCommand('EXEC'); |
|
|
|
|
337
|
|
|
//TODO: check if we should send back false or not |
338
|
|
|
return true; |
339
|
|
|
} |
340
|
|
|
|
341
|
1 |
|
/** |
342
|
|
|
* @inheritdoc |
343
|
1 |
|
*/ |
344
|
1 |
|
public function removeUser(ClientModelInterface $client, $userId) |
345
|
1 |
|
{ |
346
|
1 |
|
$key = $client->getKey(); |
347
|
1 |
|
$clientUsersListKey = $this->getClientUsersListKey($key); |
348
|
1 |
|
$userClientsListKey = $this->getUserClientsListKey($userId); |
349
|
1 |
|
$this->db->executeCommand('MULTI'); |
350
|
1 |
|
$this->db->executeCommand('SREM', [$clientUsersListKey, $userId]); |
351
|
1 |
|
$this->db->executeCommand('SREM', [$userClientsListKey, $key]); |
352
|
1 |
|
$queryResult = $this->db->executeCommand('EXEC'); |
|
|
|
|
353
|
|
|
//TODO: check if we should send back false or not |
354
|
|
|
return true; |
355
|
|
|
} |
356
|
|
|
|
357
|
|
|
/** |
358
|
|
|
* @inheritdoc |
359
|
|
|
*/ |
360
|
|
|
public function findAllByUserId($userId) |
361
|
|
|
{ |
362
|
|
|
$userClientsListKey = $this->getUserClientsListKey($userId); |
363
|
|
|
$clientsList = $this->db->executeCommand('SMEMBERS', [$userClientsListKey]); |
364
|
|
|
$clients = []; |
365
|
|
|
foreach ($clientsList as $clientId) { |
366
|
|
|
$result = $this->findOne($clientId); |
367
|
|
|
if ($result instanceof ClientModelInterface) { |
368
|
|
|
$clients[] = $result; |
369
|
|
|
} |
370
|
|
|
} |
371
|
|
|
return $clients; |
372
|
|
|
} |
373
|
|
|
|
374
|
|
|
/** |
375
|
|
|
* @inheritdoc |
376
|
|
|
*/ |
377
|
|
|
public function findAll() |
378
|
|
|
{ |
379
|
|
|
$clientListKey = $this->getClientListKey(); |
380
|
|
|
$clientList = $this->db->executeCommand('SMEMBERS', [$clientListKey]); |
381
|
|
|
$clients = []; |
382
|
|
|
foreach ($clientList as $clientId) { |
383
|
|
|
$result = $this->findOne($clientId); |
384
|
|
|
if ($result instanceof ClientModelInterface) { |
385
|
|
|
$clients[] = $result; |
386
|
|
|
} |
387
|
|
|
} |
388
|
|
|
return $clients; |
389
|
|
|
} |
390
|
|
|
} |
391
|
|
|
|
This check looks at variables that have been passed in as parameters and 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.