Passed
Push — master ( 83330b...e008b7 )
by Roeland
16:22 queued 11s
created

AuthSettingsController   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 264
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 106
dl 0
loc 264
rs 9.92
c 1
b 0
f 0
wmc 31

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 19 1
B update() 0 25 7
A wipe() 0 16 4
A publishActivity() 0 13 2
A generateRandomDeviceToken() 0 6 2
B create() 0 38 6
A getServiceNotAvailableResponse() 0 4 1
A findTokenByIdAndUser() 0 10 3
A checkAppToken() 0 2 1
A destroy() 0 17 4
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Christoph Wurst <[email protected]>
6
 * @author Daniel Kesselberg <[email protected]>
7
 * @author Fabrizio Steiner <[email protected]>
8
 * @author Greta Doci <[email protected]>
9
 * @author Joas Schilling <[email protected]>
10
 * @author Lukas Reschke <[email protected]>
11
 * @author Marcel Waldvogel <[email protected]>
12
 * @author Morris Jobke <[email protected]>
13
 * @author Robin Appelman <[email protected]>
14
 * @author Roeland Jago Douma <[email protected]>
15
 * @author Sergej Nikolaev <[email protected]>
16
 *
17
 * @license AGPL-3.0
18
 *
19
 * This code is free software: you can redistribute it and/or modify
20
 * it under the terms of the GNU Affero General Public License, version 3,
21
 * as published by the Free Software Foundation.
22
 *
23
 * This program is distributed in the hope that it will be useful,
24
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
25
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26
 * GNU Affero General Public License for more details.
27
 *
28
 * You should have received a copy of the GNU Affero General Public License, version 3,
29
 * along with this program. If not, see <http://www.gnu.org/licenses/>
30
 *
31
 */
32
33
namespace OCA\Settings\Controller;
34
35
use BadMethodCallException;
36
use OC\Authentication\Exceptions\ExpiredTokenException;
37
use OC\Authentication\Exceptions\InvalidTokenException;
38
use OC\Authentication\Exceptions\PasswordlessTokenException;
39
use OC\Authentication\Exceptions\WipeTokenException;
40
use OC\Authentication\Token\INamedToken;
41
use OC\Authentication\Token\IProvider;
42
use OC\Authentication\Token\IToken;
43
use OC\Authentication\Token\RemoteWipe;
44
use OCA\Settings\Activity\Provider;
45
use OCP\Activity\IManager;
46
use OCP\AppFramework\Controller;
47
use OCP\AppFramework\Http;
48
use OCP\AppFramework\Http\JSONResponse;
49
use OCP\IRequest;
50
use OCP\ISession;
51
use OCP\IUserSession;
52
use OCP\Security\ISecureRandom;
53
use OCP\Session\Exceptions\SessionNotAvailableException;
54
use Psr\Log\LoggerInterface;
55
56
class AuthSettingsController extends Controller {
57
58
	/** @var IProvider */
59
	private $tokenProvider;
60
61
	/** @var ISession */
62
	private $session;
63
64
	/** IUserSession */
65
	private $userSession;
66
67
	/** @var string */
68
	private $uid;
69
70
	/** @var ISecureRandom */
71
	private $random;
72
73
	/** @var IManager */
74
	private $activityManager;
75
76
	/** @var RemoteWipe */
77
	private $remoteWipe;
78
79
	/** @var LoggerInterface */
80
	private $logger;
81
82
	/**
83
	 * @param string $appName
84
	 * @param IRequest $request
85
	 * @param IProvider $tokenProvider
86
	 * @param ISession $session
87
	 * @param ISecureRandom $random
88
	 * @param string|null $userId
89
	 * @param IUserSession $userSession
90
	 * @param IManager $activityManager
91
	 * @param RemoteWipe $remoteWipe
92
	 * @param LoggerInterface $logger
93
	 */
94
	public function __construct(string $appName,
95
								IRequest $request,
96
								IProvider $tokenProvider,
97
								ISession $session,
98
								ISecureRandom $random,
99
								?string $userId,
100
								IUserSession $userSession,
101
								IManager $activityManager,
102
								RemoteWipe $remoteWipe,
103
								LoggerInterface $logger) {
104
		parent::__construct($appName, $request);
105
		$this->tokenProvider = $tokenProvider;
106
		$this->uid = $userId;
107
		$this->userSession = $userSession;
108
		$this->session = $session;
109
		$this->random = $random;
110
		$this->activityManager = $activityManager;
111
		$this->remoteWipe = $remoteWipe;
112
		$this->logger = $logger;
113
	}
114
115
	/**
116
	 * @NoAdminRequired
117
	 * @NoSubAdminRequired
118
	 * @PasswordConfirmationRequired
119
	 *
120
	 * @param string $name
121
	 * @return JSONResponse
122
	 */
123
	public function create($name) {
124
		if ($this->checkAppToken()) {
125
			return $this->getServiceNotAvailableResponse();
126
		}
127
128
		try {
129
			$sessionId = $this->session->getId();
130
		} catch (SessionNotAvailableException $ex) {
131
			return $this->getServiceNotAvailableResponse();
132
		}
133
		if ($this->userSession->getImpersonatingUserID() !== null) {
134
			return $this->getServiceNotAvailableResponse();
135
		}
136
137
		try {
138
			$sessionToken = $this->tokenProvider->getToken($sessionId);
139
			$loginName = $sessionToken->getLoginName();
140
			try {
141
				$password = $this->tokenProvider->getPassword($sessionToken, $sessionId);
142
			} catch (PasswordlessTokenException $ex) {
143
				$password = null;
144
			}
145
		} catch (InvalidTokenException $ex) {
146
			return $this->getServiceNotAvailableResponse();
147
		}
148
149
		$token = $this->generateRandomDeviceToken();
150
		$deviceToken = $this->tokenProvider->generateToken($token, $this->uid, $loginName, $password, $name, IToken::PERMANENT_TOKEN);
151
		$tokenData = $deviceToken->jsonSerialize();
152
		$tokenData['canDelete'] = true;
153
		$tokenData['canRename'] = true;
154
155
		$this->publishActivity(Provider::APP_TOKEN_CREATED, $deviceToken->getId(), ['name' => $deviceToken->getName()]);
156
157
		return new JSONResponse([
158
			'token' => $token,
159
			'loginName' => $loginName,
160
			'deviceToken' => $tokenData,
161
		]);
162
	}
163
164
	/**
165
	 * @return JSONResponse
166
	 */
167
	private function getServiceNotAvailableResponse() {
168
		$resp = new JSONResponse();
169
		$resp->setStatus(Http::STATUS_SERVICE_UNAVAILABLE);
170
		return $resp;
171
	}
172
173
	/**
174
	 * Return a 25 digit device password
175
	 *
176
	 * Example: AbCdE-fGhJk-MnPqR-sTwXy-23456
177
	 *
178
	 * @return string
179
	 */
180
	private function generateRandomDeviceToken() {
181
		$groups = [];
182
		for ($i = 0; $i < 5; $i++) {
183
			$groups[] = $this->random->generate(5, ISecureRandom::CHAR_HUMAN_READABLE);
184
		}
185
		return implode('-', $groups);
186
	}
187
188
	private function checkAppToken(): bool {
189
		return $this->session->exists('app_password');
190
	}
191
192
	/**
193
	 * @NoAdminRequired
194
	 * @NoSubAdminRequired
195
	 *
196
	 * @param int $id
197
	 * @return array|JSONResponse
198
	 */
199
	public function destroy($id) {
200
		if ($this->checkAppToken()) {
201
			return new JSONResponse([], Http::STATUS_BAD_REQUEST);
202
		}
203
204
		try {
205
			$token = $this->findTokenByIdAndUser($id);
206
		} catch (WipeTokenException $e) {
207
			//continue as we can destroy tokens in wipe
208
			$token = $e->getToken();
209
		} catch (InvalidTokenException $e) {
210
			return new JSONResponse([], Http::STATUS_NOT_FOUND);
211
		}
212
213
		$this->tokenProvider->invalidateTokenById($this->uid, $token->getId());
214
		$this->publishActivity(Provider::APP_TOKEN_DELETED, $token->getId(), ['name' => $token->getName()]);
215
		return [];
216
	}
217
218
	/**
219
	 * @NoAdminRequired
220
	 * @NoSubAdminRequired
221
	 *
222
	 * @param int $id
223
	 * @param array $scope
224
	 * @param string $name
225
	 * @return array|JSONResponse
226
	 */
227
	public function update($id, array $scope, string $name) {
228
		if ($this->checkAppToken()) {
229
			return new JSONResponse([], Http::STATUS_BAD_REQUEST);
230
		}
231
232
		try {
233
			$token = $this->findTokenByIdAndUser($id);
234
		} catch (InvalidTokenException $e) {
235
			return new JSONResponse([], Http::STATUS_NOT_FOUND);
236
		}
237
238
		$currentName = $token->getName();
239
240
		if ($scope !== $token->getScopeAsArray()) {
241
			$token->setScope(['filesystem' => $scope['filesystem']]);
242
			$this->publishActivity($scope['filesystem'] ? Provider::APP_TOKEN_FILESYSTEM_GRANTED : Provider::APP_TOKEN_FILESYSTEM_REVOKED, $token->getId(), ['name' => $currentName]);
243
		}
244
245
		if ($token instanceof INamedToken && $name !== $currentName) {
246
			$token->setName($name);
247
			$this->publishActivity(Provider::APP_TOKEN_RENAMED, $token->getId(), ['name' => $currentName, 'newName' => $name]);
248
		}
249
250
		$this->tokenProvider->updateToken($token);
251
		return [];
252
	}
253
254
	/**
255
	 * @param string $subject
256
	 * @param int $id
257
	 * @param array $parameters
258
	 */
259
	private function publishActivity(string $subject, int $id, array $parameters = []): void {
260
		$event = $this->activityManager->generateEvent();
261
		$event->setApp('settings')
262
			->setType('security')
263
			->setAffectedUser($this->uid)
264
			->setAuthor($this->uid)
265
			->setSubject($subject, $parameters)
266
			->setObject('app_token', $id, 'App Password');
267
268
		try {
269
			$this->activityManager->publish($event);
270
		} catch (BadMethodCallException $e) {
271
			$this->logger->warning('could not publish activity', ['exception' => $e]);
272
		}
273
	}
274
275
	/**
276
	 * Find a token by given id and check if uid for current session belongs to this token
277
	 *
278
	 * @param int $id
279
	 * @return IToken
280
	 * @throws InvalidTokenException
281
	 */
282
	private function findTokenByIdAndUser(int $id): IToken {
283
		try {
284
			$token = $this->tokenProvider->getTokenById($id);
285
		} catch (ExpiredTokenException $e) {
286
			$token = $e->getToken();
287
		}
288
		if ($token->getUID() !== $this->uid) {
289
			throw new InvalidTokenException('This token does not belong to you!');
290
		}
291
		return $token;
292
	}
293
294
	/**
295
	 * @NoAdminRequired
296
	 * @NoSubAdminRequired
297
	 * @PasswordConfirmationRequired
298
	 *
299
	 * @param int $id
300
	 * @return JSONResponse
301
	 * @throws InvalidTokenException
302
	 * @throws \OC\Authentication\Exceptions\ExpiredTokenException
303
	 */
304
	public function wipe(int $id): JSONResponse {
305
		if ($this->checkAppToken()) {
306
			return new JSONResponse([], Http::STATUS_BAD_REQUEST);
307
		}
308
309
		try {
310
			$token = $this->findTokenByIdAndUser($id);
311
		} catch (InvalidTokenException $e) {
312
			return new JSONResponse([], Http::STATUS_NOT_FOUND);
313
		}
314
315
		if (!$this->remoteWipe->markTokenForWipe($token)) {
316
			return new JSONResponse([], Http::STATUS_BAD_REQUEST);
317
		}
318
319
		return new JSONResponse([]);
320
	}
321
}
322