Completed
Push — master ( bedfb5...151b1b )
by Tom
19:02 queued 08:28
created

SyncBackend::configure()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 40
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 33
nc 1
nop 0
dl 0
loc 40
rs 8.8571
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author Jörn Friedrich Dreyer <[email protected]>
4
 * @author Thomas Müller <[email protected]>
5
 *
6
 * @copyright Copyright (c) 2018, ownCloud GmbH
7
 * @license AGPL-3.0
8
 *
9
 * This code is free software: you can redistribute it and/or modify
10
 * it under the terms of the GNU Affero General Public License, version 3,
11
 * as published by the Free Software Foundation.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
 * GNU Affero General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU Affero General Public License, version 3,
19
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
20
 *
21
 */
22
23
namespace OC\Core\Command\User;
24
25
26
use OC\User\AccountMapper;
27
use OC\User\Sync\AllUsersIterator;
28
use OC\User\Sync\SeenUsersIterator;
29
use OC\User\SyncService;
30
use OCP\IConfig;
31
use OCP\ILogger;
32
use OCP\IUser;
33
use OCP\IUserManager;
34
use OCP\UserInterface;
35
use Symfony\Component\Console\Command\Command;
36
use Symfony\Component\Console\Helper\ProgressBar;
37
use Symfony\Component\Console\Question\ChoiceQuestion;
38
use Symfony\Component\Console\Input\InputInterface;
39
use Symfony\Component\Console\Input\InputOption;
40
use Symfony\Component\Console\Output\OutputInterface;
41
use Symfony\Component\Console\Input\InputArgument;
42
43
class SyncBackend extends Command {
44
45
	/** @var AccountMapper */
46
	protected $accountMapper;
47
	/** @var IConfig */
48
	private $config;
49
	/** @var IUserManager */
50
	private $userManager;
51
	/** @var ILogger */
52
	private $logger;
53
54
	/**
55
	 * @param AccountMapper $accountMapper
56
	 * @param IConfig $config
57
	 * @param IUserManager $userManager
58
	 * @param ILogger $logger
59
	 */
60
	public function __construct(AccountMapper $accountMapper,
61
								IConfig $config,
62
								IUserManager $userManager,
63
								ILogger $logger) {
64
		parent::__construct();
65
		$this->accountMapper = $accountMapper;
66
		$this->config = $config;
67
		$this->userManager = $userManager;
68
		$this->logger = $logger;
69
	}
70
71
	protected function configure() {
72
		$this
73
			->setName('user:sync')
74
			->setDescription('Synchronize users from a given backend to the accounts table.')
75
			->addArgument(
76
				'backend-class',
77
				InputArgument::OPTIONAL,
78
				'The PHP class name - e.g., "OCA\User_LDAP\User_Proxy". Please wrap the class name in double quotes. You can use the option --list to list all known backend classes.'
79
			)
80
			->addOption(
81
				'list',
82
				'l',
83
				InputOption::VALUE_NONE,
84
				'List all known backend classes'
85
			)
86
			->addOption(
87
				'uid',
88
				'u',
89
				InputOption::VALUE_REQUIRED,
90
				'sync only the user with the given user id'
91
			)
92
			->addOption(
93
				'seenOnly',
94
				's',
95
				InputOption::VALUE_NONE,
96
				'sync only seen users'
97
			)
98
			->addOption(
99
				'showCount',
100
				'c',
101
				InputOption::VALUE_NONE,
102
				'calculate user count before syncing'
103
			)
104
			->addOption(
105
				'missing-account-action',
106
				'm',
107
				InputOption::VALUE_REQUIRED,
108
				'Action to take 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.'
109
			);
110
	}
111
112
	protected function execute(InputInterface $input, OutputInterface $output) {
113
		if ($input->getOption('list')) {
114
			$backends = $this->userManager->getBackends();
115
			foreach ($backends as $backend) {
116
				$output->writeln(get_class($backend));
117
			}
118
			return 0;
119
		}
120
		$backendClassName = $input->getArgument('backend-class');
121
		if ($backendClassName === null) {
122
			$output->writeln('<error>No backend class name given. Please run ./occ help user:sync to understand how this command works.</error>');
123
			return 1;
124
		}
125
		$backend = $this->getBackend($backendClassName);
126
		if ($backend === null) {
127
			$output->writeln("<error>The backend <$backendClassName> does not exist. Did you miss to enable the app?</error>");
128
			return 1;
129
		}
130
		if (!$backend->hasUserListings()) {
131
			$output->writeln("<error>The backend <$backendClassName> does not allow user listing. No sync is possible</error>");
132
			return 1;
133
		}
134
135
		$validActions = ['disable', 'remove'];
136
137
		if ($input->getOption('missing-account-action') !== null) {
138
			$missingAccountsAction = $input->getOption('missing-account-action');
139
			if (!in_array($missingAccountsAction, $validActions, true)) {
140
				$output->writeln('<error>Unknown action. Choose between "disable" or "remove"</error>');
141
				return 1;
142
			}
143
		} else {
144
			// ask (if possible) how to handle missing accounts. Disable the accounts by default.
145
			$helper = $this->getHelper('question');
146
			$question = new ChoiceQuestion(
147
					'If unknown users are found, what do you want to do with their accounts? (removing the account will also remove its data)',
148
					array_merge($validActions, ['ask later']),
149
					0
150
			);
151
			$missingAccountsAction = $helper->ask($input, $output, $question);
152
		}
153
154
		$syncService = new SyncService($this->config, $this->logger, $this->accountMapper);
155
156
		$uid = $input->getOption('uid');
157
158
		if ($uid) {
159
			$this->syncSingleUser($input, $output, $syncService, $backend, $uid, $missingAccountsAction, $validActions);
160
		} else {
161
			$this->syncMultipleUsers($input, $output, $syncService, $backend, $missingAccountsAction, $validActions);
162
		}
163
164
		return 0;
165
	}
166
167
168
	/**
169
	 * @param InputInterface $input
170
	 * @param OutputInterface $output
171
	 * @param SyncService $syncService
172
	 * @param UserInterface $backend
173
	 * @param string $missingAccountsAction
174
	 * @param array $validActions
175
	 */
176
	private function syncMultipleUsers (
177
		InputInterface $input,
178
		OutputInterface $output,
179
		SyncService $syncService,
180
		UserInterface $backend,
181
		$missingAccountsAction,
182
		array $validActions
183
	) {
184
		$output->writeln('Analyse unknown users ...');
185
		$p = new ProgressBar($output);
186
		$unknownUsers = $syncService->getNoLongerExistingUsers($backend, function () use ($p) {
187
			$p->advance();
188
		});
189
		$p->finish();
190
		$output->writeln('');
191
		$output->writeln('');
192
		$this->handleUnknownUsers($unknownUsers, $input, $output, $missingAccountsAction, $validActions);
193
194
		$output->writeln('Insert new and update existing users ...');
195
		$p = new ProgressBar($output);
196
		$max = null;
197
		if ($backend->implementsActions(\OC_User_Backend::COUNT_USERS) && $input->getOption('showCount')) {
198
			$max = $backend->countUsers();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface OCP\UserInterface as the method countUsers() does only exist in the following implementations of said interface: OCA\Testing\AlternativeHomeUserBackend, OC\User\Database.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
199
		}
200
		$p->start($max);
201
202
		if ($input->getOption('seenOnly')) {
203
			$iterator = new SeenUsersIterator($this->accountMapper, get_class($backend));
204
		} else {
205
			$iterator = new AllUsersIterator($backend);
206
		}
207
		$syncService->run($backend, $iterator, function () use ($p) {
208
			$p->advance();
209
		});
210
		$p->finish();
211
		$output->writeln('');
212
		$output->writeln('');
213
	}
214
215
	/**
216
	 * @param InputInterface $input
217
	 * @param OutputInterface $output
218
	 * @param SyncService $syncService
219
	 * @param UserInterface $backend
220
	 * @param string $uid
221
	 * @param string $missingAccountsAction
222
	 * @param array $validActions
223
	 */
224
	private function syncSingleUser(
225
		InputInterface $input,
226
		OutputInterface $output,
227
		SyncService $syncService,
228
		UserInterface $backend,
229
		$uid,
230
		$missingAccountsAction,
231
		array $validActions
232
	) {
233
		$output->writeln("Syncing $uid ...");
234
		if (!$backend->userExists($uid)) {
235
			$this->handleUnknownUsers([$uid], $input, $output, $missingAccountsAction, $validActions);
236
		} else {
237
			// sync
238
			$syncService->run($backend, new \ArrayIterator([$uid]), function (){});
239
		}
240
	}
241
	/**
242
	 * @param $backend
243
	 * @return null|UserInterface
244
	 */
245
	private function getBackend($backend) {
246
		$backends = $this->userManager->getBackends();
247
		$match = array_filter($backends, function ($b) use ($backend) {
248
			return get_class($b) === $backend;
249
		});
250
		if (empty($match)) {
251
			return null;
252
		}
253
		return array_pop($match);
254
	}
255
256
	/**
257
	 * @param array $uids a list of uids to the the action
258
	 * @param callable $callbackExists the callback used if the account for the uid exists. The
259
	 * uid and the specific account will be passed as parameter to the callback in that order
260
	 * @param callable $callbackMissing the callback used if the account doesn't exists. The uid (not
261
	 * the account) will be passed as parameter to the callback
262
	 */
263
	private function doActionForAccountUids(array $uids, callable $callbackExists, callable $callbackMissing = null) {
264
		foreach ($uids as $u) {
265
			$userAccount = $this->userManager->get($u);
266
			if ($userAccount === null) {
267
				$callbackMissing($u);
268
			} else {
269
				$callbackExists($u, $userAccount);
270
			}
271
		}
272
	}
273
274
	/**
275
	 * @param string[] $unknownUsers
276
	 * @param InputInterface $input
277
	 * @param OutputInterface $output
278
	 * @param $missingAccountsAction
279
	 * @param $validActions
280
	 */
281
	private function handleUnknownUsers(array $unknownUsers, InputInterface $input, OutputInterface $output, $missingAccountsAction, $validActions) {
282
283
		if (empty($unknownUsers)) {
284
			$output->writeln('No unknown users have been detected.');
285
		} else {
286
			$output->writeln('Following users are no longer known with the connected backend.');
287
			switch ($missingAccountsAction) {
288 View Code Duplication
				case 'disable':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
289
					$output->writeln('Proceeding to disable the accounts');
290
					$this->doActionForAccountUids($unknownUsers,
291
						function ($uid, IUser $ac) use ($output) {
292
							$ac->setEnabled(false);
293
							$output->writeln($uid);
294
						},
295
						function ($uid) use ($output) {
296
							$output->writeln("$uid (unknown account for the user)");
297
						});
298
					break;
299 View Code Duplication
				case 'remove':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
300
					$output->writeln('Proceeding to remove the accounts');
301
					$this->doActionForAccountUids($unknownUsers,
302
						function ($uid, IUser $ac) use ($output) {
303
							$ac->delete();
304
							$output->writeln($uid);
305
						},
306
						function ($uid) use ($output) {
307
							$output->writeln("$uid (unknown account for the user)");
308
						});
309
					break;
310
				case 'ask later':
311
					$output->writeln('listing the unknown accounts');
312
					$this->doActionForAccountUids($unknownUsers,
313
						function ($uid) use ($output) {
314
							$output->writeln($uid);
315
						},
316
						function ($uid) use ($output) {
317
							$output->writeln("$uid (unknown account for the user)");
318
						});
319
					// overwriting variables!
320
					$helper = $this->getHelper('question');
321
					$question = new ChoiceQuestion(
322
						'What do you want to do with their accounts? (removing the account will also remove its data)',
323
						$validActions,
324
						0
325
					);
326
					$missingAccountsAction2 = $helper->ask($input, $output, $question);
327
					switch ($missingAccountsAction2) {
328
						// if "nothing" is selected, just ignore and finish
329 View Code Duplication
						case 'disable':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
330
							$output->writeln('Proceeding to disable the accounts');
331
							$this->doActionForAccountUids($unknownUsers,
332
								function ($uid, IUser $ac) {
333
									$ac->setEnabled(false);
334
								},
335
								function ($uid) use ($output) {
336
									$output->writeln("$uid (unknown account for the user)");
337
								});
338
							break;
339 View Code Duplication
						case 'remove':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
340
							$output->writeln('Proceeding to remove the accounts');
341
							$this->doActionForAccountUids($unknownUsers,
342
								function ($uid, IUser $ac) {
343
									$ac->delete();
344
								},
345
								function ($uid) use ($output) {
346
									$output->writeln("$uid (unknown account for the user)");
347
								});
348
							break;
349
					}
350
					break;
351
			}
352
		}
353
	}
354
}
355