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

SecurityMiddleware::hasAnnotationOrAttribute()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 3
nop 3
dl 0
loc 10
rs 10
c 0
b 0
f 0
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