Passed
Push — master ( d60172...75f17b )
by Joas
16:26 queued 12s
created

SecurityMiddleware::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 27
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 13
nc 1
nop 13
dl 0
loc 27
rs 9.8333
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * @copyright Copyright (c) 2016, ownCloud, Inc.
7
 *
8
 * @author Bernhard Posselt <[email protected]>
9
 * @author Bjoern Schiessle <[email protected]>
10
 * @author Christoph Wurst <[email protected]>
11
 * @author Daniel Kesselberg <[email protected]>
12
 * @author Holger Hees <[email protected]>
13
 * @author Joas Schilling <[email protected]>
14
 * @author Julien Veyssier <[email protected]>
15
 * @author Lukas Reschke <[email protected]>
16
 * @author Morris Jobke <[email protected]>
17
 * @author Roeland Jago Douma <[email protected]>
18
 * @author Stefan Weil <[email protected]>
19
 * @author Thomas Müller <[email protected]>
20
 * @author Thomas Tanghus <[email protected]>
21
 *
22
 * @license AGPL-3.0
23
 *
24
 * This code is free software: you can redistribute it and/or modify
25
 * it under the terms of the GNU Affero General Public License, version 3,
26
 * as published by the Free Software Foundation.
27
 *
28
 * This program is distributed in the hope that it will be useful,
29
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
30
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31
 * GNU Affero General Public License for more details.
32
 *
33
 * You should have received a copy of the GNU Affero General Public License, version 3,
34
 * along with this program. If not, see <http://www.gnu.org/licenses/>
35
 *
36
 */
37
38
namespace OC\AppFramework\Middleware\Security;
39
40
use OC\AppFramework\Middleware\Security\Exceptions\AppNotEnabledException;
41
use OC\AppFramework\Middleware\Security\Exceptions\CrossSiteRequestForgeryException;
42
use OC\AppFramework\Middleware\Security\Exceptions\NotAdminException;
43
use OC\AppFramework\Middleware\Security\Exceptions\NotLoggedInException;
44
use OC\AppFramework\Middleware\Security\Exceptions\SecurityException;
45
use OC\AppFramework\Middleware\Security\Exceptions\StrictCookieMissingException;
46
use OC\AppFramework\Utility\ControllerMethodReflector;
47
use OC\Settings\AuthorizedGroupMapper;
48
use OCP\App\AppPathNotFoundException;
49
use OCP\App\IAppManager;
50
use OCP\AppFramework\Controller;
51
use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting;
52
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
53
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
54
use OCP\AppFramework\Http\Attribute\PublicPage;
55
use OCP\AppFramework\Http\Attribute\StrictCookiesRequired;
56
use OCP\AppFramework\Http\Attribute\SubAdminRequired;
57
use OCP\AppFramework\Http\JSONResponse;
58
use OCP\AppFramework\Http\RedirectResponse;
59
use OCP\AppFramework\Http\Response;
60
use OCP\AppFramework\Http\TemplateResponse;
61
use OCP\AppFramework\Middleware;
62
use OCP\AppFramework\OCSController;
63
use OCP\IL10N;
64
use OCP\INavigationManager;
65
use OCP\IRequest;
66
use OCP\IURLGenerator;
67
use OCP\IUserSession;
68
use OCP\Util;
69
use Psr\Log\LoggerInterface;
70
use ReflectionMethod;
71
72
/**
73
 * Used to do all the authentication and checking stuff for a controller method
74
 * It reads out the annotations of a controller method and checks which if
75
 * security things should be checked and also handles errors in case a security
76
 * check fails
77
 */
78
class SecurityMiddleware extends Middleware {
79
	/** @var INavigationManager */
80
	private $navigationManager;
81
	/** @var IRequest */
82
	private $request;
83
	/** @var ControllerMethodReflector */
84
	private $reflector;
85
	/** @var string */
86
	private $appName;
87
	/** @var IURLGenerator */
88
	private $urlGenerator;
89
	/** @var LoggerInterface */
90
	private $logger;
91
	/** @var bool */
92
	private $isLoggedIn;
93
	/** @var bool */
94
	private $isAdminUser;
95
	/** @var bool */
96
	private $isSubAdmin;
97
	/** @var IAppManager */
98
	private $appManager;
99
	/** @var IL10N */
100
	private $l10n;
101
	/** @var AuthorizedGroupMapper */
102
	private $groupAuthorizationMapper;
103
	/** @var IUserSession */
104
	private $userSession;
105
106
	public function __construct(IRequest $request,
107
								ControllerMethodReflector $reflector,
108
								INavigationManager $navigationManager,
109
								IURLGenerator $urlGenerator,
110
								LoggerInterface $logger,
111
								string $appName,
112
								bool $isLoggedIn,
113
								bool $isAdminUser,
114
								bool $isSubAdmin,
115
								IAppManager $appManager,
116
								IL10N $l10n,
117
								AuthorizedGroupMapper $mapper,
118
								IUserSession $userSession
119
	) {
120
		$this->navigationManager = $navigationManager;
121
		$this->request = $request;
122
		$this->reflector = $reflector;
123
		$this->appName = $appName;
124
		$this->urlGenerator = $urlGenerator;
125
		$this->logger = $logger;
126
		$this->isLoggedIn = $isLoggedIn;
127
		$this->isAdminUser = $isAdminUser;
128
		$this->isSubAdmin = $isSubAdmin;
129
		$this->appManager = $appManager;
130
		$this->l10n = $l10n;
131
		$this->groupAuthorizationMapper = $mapper;
132
		$this->userSession = $userSession;
133
	}
134
135
	/**
136
	 * This runs all the security checks before a method call. The
137
	 * security checks are determined by inspecting the controller method
138
	 * annotations
139
	 *
140
	 * @param Controller $controller the controller
141
	 * @param string $methodName the name of the method
142
	 * @throws SecurityException when a security check fails
143
	 *
144
	 * @suppress PhanUndeclaredClassConstant
145
	 */
146
	public function beforeController($controller, $methodName) {
147
		// this will set the current navigation entry of the app, use this only
148
		// for normal HTML requests and not for AJAX requests
149
		$this->navigationManager->setActiveEntry($this->appName);
150
151
		if (get_class($controller) === \OCA\Talk\Controller\PageController::class && $methodName === 'showCall') {
0 ignored issues
show
Bug introduced by
The type OCA\Talk\Controller\PageController was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
152
			$this->navigationManager->setActiveEntry('spreed');
153
		}
154
155
		$reflectionMethod = new ReflectionMethod($controller, $methodName);
156
157
		// security checks
158
		$isPublicPage = $this->hasAnnotationOrAttribute($reflectionMethod, 'PublicPage', PublicPage::class);
159
		if (!$isPublicPage) {
160
			if (!$this->isLoggedIn) {
161
				throw new NotLoggedInException();
162
			}
163
			$authorized = false;
164
			if ($this->hasAnnotationOrAttribute($reflectionMethod, 'AuthorizedAdminSetting', AuthorizedAdminSetting::class)) {
165
				$authorized = $this->isAdminUser;
166
167
				if (!$authorized && $this->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class)) {
168
					$authorized = $this->isSubAdmin;
169
				}
170
171
				if (!$authorized) {
172
					$settingClasses = $this->getAuthorizedAdminSettingClasses($reflectionMethod);
173
					$authorizedClasses = $this->groupAuthorizationMapper->findAllClassesForUser($this->userSession->getUser());
174
					foreach ($settingClasses as $settingClass) {
175
						$authorized = in_array($settingClass, $authorizedClasses, true);
176
177
						if ($authorized) {
178
							break;
179
						}
180
					}
181
				}
182
				if (!$authorized) {
183
					throw new NotAdminException($this->l10n->t('Logged in user must be an admin, a sub admin or gotten special right to access this setting'));
184
				}
185
			}
186
			if ($this->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class)
187
				&& !$this->isSubAdmin
188
				&& !$this->isAdminUser
189
				&& !$authorized) {
190
				throw new NotAdminException($this->l10n->t('Logged in user must be an admin or sub admin'));
191
			}
192
			if (!$this->hasAnnotationOrAttribute($reflectionMethod, 'SubAdminRequired', SubAdminRequired::class)
193
				&& !$this->hasAnnotationOrAttribute($reflectionMethod, 'NoAdminRequired', NoAdminRequired::class)
194
				&& !$this->isAdminUser
195
				&& !$authorized) {
196
				throw new NotAdminException($this->l10n->t('Logged in user must be an admin'));
197
			}
198
		}
199
200
		// Check for strict cookie requirement
201
		if ($this->hasAnnotationOrAttribute($reflectionMethod, 'StrictCookieRequired', StrictCookiesRequired::class) ||
202
			!$this->hasAnnotationOrAttribute($reflectionMethod, 'NoCSRFRequired', NoCSRFRequired::class)) {
203
			if (!$this->request->passesStrictCookieCheck()) {
204
				throw new StrictCookieMissingException();
205
			}
206
		}
207
		// CSRF check - also registers the CSRF token since the session may be closed later
208
		Util::callRegister();
209
		if (!$this->hasAnnotationOrAttribute($reflectionMethod, 'NoCSRFRequired', NoCSRFRequired::class)) {
210
			/*
211
			 * Only allow the CSRF check to fail on OCS Requests. This kind of
212
			 * hacks around that we have no full token auth in place yet and we
213
			 * do want to offer CSRF checks for web requests.
214
			 *
215
			 * Additionally we allow Bearer authenticated requests to pass on OCS routes.
216
			 * This allows oauth apps (e.g. moodle) to use the OCS endpoints
217
			 */
218
			if (!$this->request->passesCSRFCheck() && !(
219
				$controller instanceof OCSController && (
220
					$this->request->getHeader('OCS-APIREQUEST') === 'true' ||
221
					strpos($this->request->getHeader('Authorization'), 'Bearer ') === 0
222
				)
223
			)) {
224
				throw new CrossSiteRequestForgeryException();
225
			}
226
		}
227
228
		/**
229
		 * Checks if app is enabled (also includes a check whether user is allowed to access the resource)
230
		 * The getAppPath() check is here since components such as settings also use the AppFramework and
231
		 * therefore won't pass this check.
232
		 * If page is public, app does not need to be enabled for current user/visitor
233
		 */
234
		try {
235
			$appPath = $this->appManager->getAppPath($this->appName);
236
		} catch (AppPathNotFoundException $e) {
237
			$appPath = false;
238
		}
239
240
		if ($appPath !== false && !$isPublicPage && !$this->appManager->isEnabledForUser($this->appName)) {
241
			throw new AppNotEnabledException();
242
		}
243
	}
244
245
	/**
246
	 * @template T
247
	 *
248
	 * @param ReflectionMethod $reflectionMethod
249
	 * @param string $annotationName
250
	 * @param class-string<T> $attributeClass
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<T> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<T>.
Loading history...
251
	 * @return boolean
252
	 */
253
	protected function hasAnnotationOrAttribute(ReflectionMethod $reflectionMethod, string $annotationName, string $attributeClass): bool {
254
		if (!empty($reflectionMethod->getAttributes($attributeClass))) {
255
			return true;
256
		}
257
258
		if ($this->reflector->hasAnnotation($annotationName)) {
259
			return true;
260
		}
261
262
		return false;
263
	}
264
265
	/**
266
	 * @param ReflectionMethod $reflectionMethod
267
	 * @return string[]
268
	 */
269
	protected function getAuthorizedAdminSettingClasses(ReflectionMethod $reflectionMethod): array {
270
		$classes = [];
271
		if ($this->reflector->hasAnnotation('AuthorizedAdminSetting')) {
272
			$classes = explode(';', $this->reflector->getAnnotationParameter('AuthorizedAdminSetting', 'settings'));
273
		}
274
275
		$attributes = $reflectionMethod->getAttributes(AuthorizedAdminSetting::class);
276
		if (!empty($attributes)) {
277
			foreach ($attributes as $attribute) {
278
				/** @var AuthorizedAdminSetting $setting */
279
				$setting = $attribute->newInstance();
280
				$classes[] = $setting->getSettings();
281
			}
282
		}
283
284
		return $classes;
285
	}
286
287
	/**
288
	 * If an SecurityException is being caught, ajax requests return a JSON error
289
	 * response and non ajax requests redirect to the index
290
	 *
291
	 * @param Controller $controller the controller that is being called
292
	 * @param string $methodName the name of the method that will be called on
293
	 *                           the controller
294
	 * @param \Exception $exception the thrown exception
295
	 * @return Response a Response object or null in case that the exception could not be handled
296
	 * @throws \Exception the passed in exception if it can't handle it
297
	 */
298
	public function afterException($controller, $methodName, \Exception $exception): Response {
299
		if ($exception instanceof SecurityException) {
300
			if ($exception instanceof StrictCookieMissingException) {
301
				return new RedirectResponse(\OC::$WEBROOT . '/');
302
			}
303
			if (stripos($this->request->getHeader('Accept'), 'html') === false) {
304
				$response = new JSONResponse(
305
					['message' => $exception->getMessage()],
306
					$exception->getCode()
307
				);
308
			} else {
309
				if ($exception instanceof NotLoggedInException) {
310
					$params = [];
311
					if (isset($this->request->server['REQUEST_URI'])) {
312
						$params['redirect_url'] = $this->request->server['REQUEST_URI'];
313
					}
314
					$usernamePrefill = $this->request->getParam('user', '');
315
					if ($usernamePrefill !== '') {
316
						$params['user'] = $usernamePrefill;
317
					}
318
					if ($this->request->getParam('direct')) {
319
						$params['direct'] = 1;
320
					}
321
					$url = $this->urlGenerator->linkToRoute('core.login.showLoginForm', $params);
322
					$response = new RedirectResponse($url);
323
				} else {
324
					$response = new TemplateResponse('core', '403', ['message' => $exception->getMessage()], 'guest');
325
					$response->setStatus($exception->getCode());
326
				}
327
			}
328
329
			$this->logger->debug($exception->getMessage(), [
330
				'exception' => $exception,
331
			]);
332
			return $response;
333
		}
334
335
		throw $exception;
336
	}
337
}
338