|
1
|
|
|
<?php |
|
2
|
|
|
/** |
|
3
|
|
|
* @author Thomas Müller <[email protected]> |
|
4
|
|
|
* |
|
5
|
|
|
* @copyright Copyright (c) 2017, ownCloud GmbH |
|
6
|
|
|
* @license AGPL-3.0 |
|
7
|
|
|
* |
|
8
|
|
|
* This code is free software: you can redistribute it and/or modify |
|
9
|
|
|
* it under the terms of the GNU Affero General Public License, version 3, |
|
10
|
|
|
* as published by the Free Software Foundation. |
|
11
|
|
|
* |
|
12
|
|
|
* This program is distributed in the hope that it will be useful, |
|
13
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
14
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
15
|
|
|
* GNU Affero General Public License for more details. |
|
16
|
|
|
* |
|
17
|
|
|
* You should have received a copy of the GNU Affero General Public License, version 3, |
|
18
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/> |
|
19
|
|
|
* |
|
20
|
|
|
*/ |
|
21
|
|
|
|
|
22
|
|
|
namespace OC\Core\Command\User; |
|
23
|
|
|
|
|
24
|
|
|
|
|
25
|
|
|
use OC\User\AccountMapper; |
|
26
|
|
|
use OC\User\SyncService; |
|
27
|
|
|
use OCP\IConfig; |
|
28
|
|
|
use OCP\ILogger; |
|
29
|
|
|
use OCP\IUser; |
|
30
|
|
|
use OCP\IUserManager; |
|
31
|
|
|
use OCP\UserInterface; |
|
32
|
|
|
use Symfony\Component\Console\Command\Command; |
|
33
|
|
|
use Symfony\Component\Console\Helper\ProgressBar; |
|
34
|
|
|
use Symfony\Component\Console\Question\ChoiceQuestion; |
|
35
|
|
|
use Symfony\Component\Console\Input\InputInterface; |
|
36
|
|
|
use Symfony\Component\Console\Input\InputOption; |
|
37
|
|
|
use Symfony\Component\Console\Output\OutputInterface; |
|
38
|
|
|
use Symfony\Component\Console\Input\InputArgument; |
|
39
|
|
|
|
|
40
|
|
|
class SyncBackend extends Command { |
|
41
|
|
|
|
|
42
|
|
|
/** @var AccountMapper */ |
|
43
|
|
|
protected $accountMapper; |
|
44
|
|
|
/** @var IConfig */ |
|
45
|
|
|
private $config; |
|
46
|
|
|
/** @var IUserManager */ |
|
47
|
|
|
private $userManager; |
|
48
|
|
|
/** @var ILogger */ |
|
49
|
|
|
private $logger; |
|
50
|
|
|
|
|
51
|
|
|
/** |
|
52
|
|
|
* @param AccountMapper $accountMapper |
|
53
|
|
|
* @param IConfig $config |
|
54
|
|
|
* @param IUserManager $userManager |
|
55
|
|
|
* @param ILogger $logger |
|
56
|
|
|
*/ |
|
57
|
|
|
public function __construct(AccountMapper $accountMapper, |
|
58
|
|
|
IConfig $config, |
|
59
|
|
|
IUserManager $userManager, |
|
60
|
|
|
ILogger $logger) { |
|
61
|
|
|
parent::__construct(); |
|
62
|
|
|
$this->accountMapper = $accountMapper; |
|
63
|
|
|
$this->config = $config; |
|
64
|
|
|
$this->userManager = $userManager; |
|
65
|
|
|
$this->logger = $logger; |
|
66
|
|
|
} |
|
67
|
|
|
|
|
68
|
|
|
protected function configure() { |
|
69
|
|
|
$this |
|
70
|
|
|
->setName('user:sync') |
|
71
|
|
|
->setDescription('synchronize users from a given backend to the accounts table') |
|
72
|
|
|
->addArgument( |
|
73
|
|
|
'backend-class', |
|
74
|
|
|
InputArgument::OPTIONAL, |
|
75
|
|
|
'The php class name - e.g. "OCA\User_LDAP\User_LDAP". Please wrap the class name into double quotes. You can use the option --list to list all known backend classes' |
|
76
|
|
|
) |
|
77
|
|
|
->addOption('list', 'l', InputOption::VALUE_NONE, 'list all known backend classes') |
|
78
|
|
|
->addOption('missing-account-action', 'm', InputOption::VALUE_REQUIRED, 'action to do if the account isn\'t connected to a backend any longer. Options are "disable" and "remove". Use quotes. Note that removing the account will also remove the stored data and files for that account'); |
|
79
|
|
|
} |
|
80
|
|
|
|
|
81
|
|
|
protected function execute(InputInterface $input, OutputInterface $output) { |
|
82
|
|
|
if ($input->getOption('list')) { |
|
83
|
|
|
$backends = $this->userManager->getBackends(); |
|
84
|
|
|
foreach ($backends as $backend) { |
|
85
|
|
|
$output->writeln(get_class($backend)); |
|
86
|
|
|
} |
|
87
|
|
|
return 0; |
|
88
|
|
|
} |
|
89
|
|
|
$backendClassName = $input->getArgument('backend-class'); |
|
90
|
|
|
if (is_null($backendClassName)) { |
|
91
|
|
|
$output->writeln("<error>No backend class name given. Please run ./occ help user:sync to understand how this command works.</error>"); |
|
92
|
|
|
return 1; |
|
93
|
|
|
} |
|
94
|
|
|
$backend = $this->getBackend($backendClassName); |
|
95
|
|
|
if (is_null($backend)) { |
|
96
|
|
|
$output->writeln("<error>The backend <$backendClassName> does not exist. Did you miss to enable the app?</error>"); |
|
97
|
|
|
return 1; |
|
98
|
|
|
} |
|
99
|
|
|
if (!$backend->hasUserListings()) { |
|
100
|
|
|
$output->writeln("<error>The backend <$backendClassName> does not allow user listing. No sync is possible</error>"); |
|
101
|
|
|
return 1; |
|
102
|
|
|
} |
|
103
|
|
|
|
|
104
|
|
|
$validActions = ['disable', 'remove']; |
|
105
|
|
|
|
|
106
|
|
|
if ($input->getOption('missing-account-action') !== null) { |
|
107
|
|
|
$missingAccountsAction = $input->getOption('missing-account-action'); |
|
108
|
|
|
if (!in_array($missingAccountsAction, $validActions, true)) { |
|
109
|
|
|
$output->writeln("<error>Unknown action. Choose between \"disable\" or \"remove\"</error>"); |
|
110
|
|
|
return 1; |
|
111
|
|
|
} |
|
112
|
|
|
} else { |
|
113
|
|
|
// ask (if possible) how to handle missing accounts. Disable the accounts by default. |
|
114
|
|
|
$helper = $this->getHelper('question'); |
|
115
|
|
|
$question = new ChoiceQuestion( |
|
116
|
|
|
'If unknown users are found, what do you want to do with their accounts? (removing the account will also remove its data)', |
|
117
|
|
|
array_merge($validActions, ['ask later']), |
|
118
|
|
|
0 |
|
119
|
|
|
); |
|
120
|
|
|
$missingAccountsAction = $helper->ask($input, $output, $question); |
|
121
|
|
|
} |
|
122
|
|
|
|
|
123
|
|
|
$syncService = new SyncService($this->accountMapper, $backend, $this->config, $this->logger); |
|
124
|
|
|
|
|
125
|
|
|
// insert/update known users |
|
126
|
|
|
$output->writeln("Insert new and update existing users ..."); |
|
127
|
|
|
$p = new ProgressBar($output); |
|
128
|
|
|
$max = null; |
|
129
|
|
|
if ($backend->implementsActions(\OC_User_Backend::COUNT_USERS)) { |
|
130
|
|
|
$max = $backend->countUsers(); |
|
131
|
|
|
} |
|
132
|
|
|
$p->start($max); |
|
133
|
|
|
$syncService->run(function () use ($p) { |
|
134
|
|
|
$p->advance(); |
|
135
|
|
|
}); |
|
136
|
|
|
$p->finish(); |
|
137
|
|
|
$output->writeln(''); |
|
138
|
|
|
$output->writeln(''); |
|
139
|
|
|
|
|
140
|
|
|
// analyse unknown users |
|
141
|
|
|
$this->handleUnknownUsers($input, $output, $syncService, $missingAccountsAction, $validActions); |
|
142
|
|
|
|
|
143
|
|
|
return 0; |
|
144
|
|
|
} |
|
145
|
|
|
|
|
146
|
|
|
/** |
|
147
|
|
|
* @param $backend |
|
148
|
|
|
* @return null|UserInterface |
|
149
|
|
|
*/ |
|
150
|
|
|
private function getBackend($backend) { |
|
151
|
|
|
$backends = $this->userManager->getBackends(); |
|
152
|
|
|
$match = array_filter($backends, function ($b) use ($backend) { |
|
153
|
|
|
return get_class($b) === $backend; |
|
154
|
|
|
}); |
|
155
|
|
|
if (empty($match)) { |
|
156
|
|
|
return null; |
|
157
|
|
|
} |
|
158
|
|
|
return array_pop($match); |
|
159
|
|
|
} |
|
160
|
|
|
|
|
161
|
|
|
/** |
|
162
|
|
|
* @param array $uids a list of uids to the the action |
|
163
|
|
|
* @param callable $callbackExists the callback used if the account for the uid exists. The |
|
164
|
|
|
* uid and the specific account will be passed as parameter to the callback in that order |
|
165
|
|
|
* @param callable $callbackMissing the callback used if the account doesn't exists. The uid (not |
|
166
|
|
|
* the account) will be passed as parameter to the callback |
|
167
|
|
|
*/ |
|
168
|
|
|
private function doActionForAccountUids(array $uids, callable $callbackExists, callable $callbackMissing = null) { |
|
169
|
|
|
foreach ($uids as $u) { |
|
170
|
|
|
$userAccount = $this->userManager->get($u); |
|
171
|
|
|
if ($userAccount === null) { |
|
172
|
|
|
$callbackMissing($u); |
|
173
|
|
|
} else { |
|
174
|
|
|
$callbackExists($u, $userAccount); |
|
175
|
|
|
} |
|
176
|
|
|
} |
|
177
|
|
|
} |
|
178
|
|
|
|
|
179
|
|
|
/** |
|
180
|
|
|
* @param InputInterface $input |
|
181
|
|
|
* @param OutputInterface $output |
|
182
|
|
|
* @param $syncService |
|
183
|
|
|
* @param $missingAccountsAction |
|
184
|
|
|
* @param $validActions |
|
185
|
|
|
*/ |
|
186
|
|
|
private function handleUnknownUsers(InputInterface $input, OutputInterface $output, $syncService, $missingAccountsAction, $validActions) { |
|
187
|
|
|
$output->writeln("Analyse unknown users ..."); |
|
188
|
|
|
$p = new ProgressBar($output); |
|
189
|
|
|
$toBeDeleted = $syncService->getNoLongerExistingUsers(function () use ($p) { |
|
190
|
|
|
$p->advance(); |
|
191
|
|
|
}); |
|
192
|
|
|
$p->finish(); |
|
193
|
|
|
$output->writeln(''); |
|
194
|
|
|
$output->writeln(''); |
|
195
|
|
|
|
|
196
|
|
|
if (empty($toBeDeleted)) { |
|
197
|
|
|
$output->writeln("No unknown users have been detected."); |
|
198
|
|
|
} else { |
|
199
|
|
|
$output->writeln("Following users are no longer known with the connected backend."); |
|
200
|
|
|
switch ($missingAccountsAction) { |
|
201
|
|
View Code Duplication |
case 'disable': |
|
|
|
|
|
|
202
|
|
|
$output->writeln("Proceeding to disable the accounts"); |
|
203
|
|
|
$this->doActionForAccountUids($toBeDeleted, |
|
204
|
|
|
function ($uid, IUser $ac) use ($output) { |
|
205
|
|
|
$ac->setEnabled(false); |
|
206
|
|
|
$output->writeln($uid); |
|
207
|
|
|
}, |
|
208
|
|
|
function ($uid) use ($output) { |
|
209
|
|
|
$output->writeln($uid . " (unknown account for the user)"); |
|
210
|
|
|
}); |
|
211
|
|
|
break; |
|
212
|
|
View Code Duplication |
case 'remove': |
|
|
|
|
|
|
213
|
|
|
$output->writeln("Proceeding to remove the accounts"); |
|
214
|
|
|
$this->doActionForAccountUids($toBeDeleted, |
|
215
|
|
|
function ($uid, IUser $ac) use ($output) { |
|
216
|
|
|
$ac->delete(); |
|
217
|
|
|
$output->writeln($uid); |
|
218
|
|
|
}, |
|
219
|
|
|
function ($uid) use ($output) { |
|
220
|
|
|
$output->writeln($uid . " (unknown account for the user)"); |
|
221
|
|
|
}); |
|
222
|
|
|
break; |
|
223
|
|
|
case 'ask later': |
|
224
|
|
|
$output->writeln("listing the unknown accounts"); |
|
225
|
|
|
$this->doActionForAccountUids($toBeDeleted, |
|
226
|
|
|
function ($uid) use ($output) { |
|
227
|
|
|
$output->writeln($uid); |
|
228
|
|
|
}, |
|
229
|
|
|
function ($uid) use ($output) { |
|
230
|
|
|
$output->writeln($uid . " (unknown account for the user)"); |
|
231
|
|
|
}); |
|
232
|
|
|
// overwriting variables! |
|
233
|
|
|
$helper = $this->getHelper('question'); |
|
234
|
|
|
$question = new ChoiceQuestion( |
|
235
|
|
|
'What do you want to do with their accounts? (removing the account will also remove its data)', |
|
236
|
|
|
$validActions, |
|
237
|
|
|
0 |
|
238
|
|
|
); |
|
239
|
|
|
$missingAccountsAction2 = $helper->ask($input, $output, $question); |
|
240
|
|
|
switch ($missingAccountsAction2) { |
|
241
|
|
|
// if "nothing" is selected, just ignore and finish |
|
242
|
|
View Code Duplication |
case 'disable': |
|
|
|
|
|
|
243
|
|
|
$output->writeln("Proceeding to disable the accounts"); |
|
244
|
|
|
$this->doActionForAccountUids($toBeDeleted, |
|
245
|
|
|
function ($uid, IUser $ac) { |
|
246
|
|
|
$ac->setEnabled(false); |
|
247
|
|
|
}, |
|
248
|
|
|
function ($uid) use ($output) { |
|
249
|
|
|
$output->writeln($uid . " (unknown account for the user)"); |
|
250
|
|
|
}); |
|
251
|
|
|
break; |
|
252
|
|
View Code Duplication |
case 'remove': |
|
|
|
|
|
|
253
|
|
|
$output->writeln("Proceeding to remove the accounts"); |
|
254
|
|
|
$this->doActionForAccountUids($toBeDeleted, |
|
255
|
|
|
function ($uid, IUser $ac) { |
|
256
|
|
|
$ac->delete(); |
|
257
|
|
|
}, |
|
258
|
|
|
function ($uid) use ($output) { |
|
259
|
|
|
$output->writeln($uid . " (unknown account for the user)"); |
|
260
|
|
|
}); |
|
261
|
|
|
break; |
|
262
|
|
|
} |
|
263
|
|
|
break; |
|
264
|
|
|
} |
|
265
|
|
|
} |
|
266
|
|
|
} |
|
267
|
|
|
} |
|
268
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.