Passed
Push — master ( 96e892...5b0e70 )
by Roeland
13:09
created

SecurityMiddleware::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 29
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 14
nc 1
nop 14
dl 0
loc 29
rs 9.7998
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
declare(strict_types=1);
3
/**
4
 * @copyright Copyright (c) 2016, ownCloud, Inc.
5
 *
6
 * @author Bernhard Posselt <[email protected]>
7
 * @author Joas Schilling <[email protected]>
8
 * @author Lukas Reschke <[email protected]>
9
 * @author Morris Jobke <[email protected]>
10
 * @author Roeland Jago Douma <[email protected]>
11
 * @author Stefan Weil <[email protected]>
12
 * @author Thomas Müller <[email protected]>
13
 * @author Thomas Tanghus <[email protected]>
14
 *
15
 * @license AGPL-3.0
16
 *
17
 * This code is free software: you can redistribute it and/or modify
18
 * it under the terms of the GNU Affero General Public License, version 3,
19
 * as published by the Free Software Foundation.
20
 *
21
 * This program is distributed in the hope that it will be useful,
22
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
23
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24
 * GNU Affero General Public License for more details.
25
 *
26
 * You should have received a copy of the GNU Affero General Public License, version 3,
27
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
28
 *
29
 */
30
31
32
namespace OC\AppFramework\Middleware\Security;
33
34
use OC\AppFramework\Middleware\Security\Exceptions\AppNotEnabledException;
35
use OC\AppFramework\Middleware\Security\Exceptions\CrossSiteRequestForgeryException;
36
use OC\AppFramework\Middleware\Security\Exceptions\NotAdminException;
37
use OC\AppFramework\Middleware\Security\Exceptions\NotLoggedInException;
38
use OC\AppFramework\Middleware\Security\Exceptions\StrictCookieMissingException;
39
use OC\AppFramework\Utility\ControllerMethodReflector;
40
use OC\Security\CSP\ContentSecurityPolicyManager;
41
use OC\Security\CSP\ContentSecurityPolicyNonceManager;
42
use OC\Security\CSRF\CsrfTokenManager;
43
use OCP\App\AppPathNotFoundException;
44
use OCP\App\IAppManager;
45
use OCP\AppFramework\Http\ContentSecurityPolicy;
46
use OCP\AppFramework\Http\EmptyContentSecurityPolicy;
47
use OCP\AppFramework\Http\RedirectResponse;
48
use OCP\AppFramework\Http\TemplateResponse;
49
use OCP\AppFramework\Middleware;
50
use OCP\AppFramework\Http\Response;
51
use OCP\AppFramework\Http\JSONResponse;
52
use OCP\AppFramework\OCSController;
53
use OCP\IL10N;
54
use OCP\INavigationManager;
55
use OCP\IURLGenerator;
56
use OCP\IRequest;
57
use OCP\ILogger;
58
use OCP\AppFramework\Controller;
59
use OCP\Util;
60
use OC\AppFramework\Middleware\Security\Exceptions\SecurityException;
61
62
/**
63
 * Used to do all the authentication and checking stuff for a controller method
64
 * It reads out the annotations of a controller method and checks which if
65
 * security things should be checked and also handles errors in case a security
66
 * check fails
67
 */
68
class SecurityMiddleware extends Middleware {
69
	/** @var INavigationManager */
70
	private $navigationManager;
71
	/** @var IRequest */
72
	private $request;
73
	/** @var ControllerMethodReflector */
74
	private $reflector;
75
	/** @var string */
76
	private $appName;
77
	/** @var IURLGenerator */
78
	private $urlGenerator;
79
	/** @var ILogger */
80
	private $logger;
81
	/** @var bool */
82
	private $isLoggedIn;
83
	/** @var bool */
84
	private $isAdminUser;
85
	/** @var bool */
86
	private $isSubAdmin;
87
	/** @var ContentSecurityPolicyManager */
88
	private $contentSecurityPolicyManager;
89
	/** @var CsrfTokenManager */
90
	private $csrfTokenManager;
91
	/** @var ContentSecurityPolicyNonceManager */
92
	private $cspNonceManager;
93
	/** @var IAppManager */
94
	private $appManager;
95
	/** @var IL10N */
96
	private $l10n;
97
98
	public function __construct(IRequest $request,
99
								ControllerMethodReflector $reflector,
100
								INavigationManager $navigationManager,
101
								IURLGenerator $urlGenerator,
102
								ILogger $logger,
103
								string $appName,
104
								bool $isLoggedIn,
105
								bool $isAdminUser,
106
								bool $isSubAdmin,
107
								ContentSecurityPolicyManager $contentSecurityPolicyManager,
108
								CsrfTokenManager $csrfTokenManager,
109
								ContentSecurityPolicyNonceManager $cspNonceManager,
110
								IAppManager $appManager,
111
								IL10N $l10n
112
	) {
113
		$this->navigationManager = $navigationManager;
114
		$this->request = $request;
115
		$this->reflector = $reflector;
116
		$this->appName = $appName;
117
		$this->urlGenerator = $urlGenerator;
118
		$this->logger = $logger;
119
		$this->isLoggedIn = $isLoggedIn;
120
		$this->isAdminUser = $isAdminUser;
121
		$this->isSubAdmin = $isSubAdmin;
122
		$this->contentSecurityPolicyManager = $contentSecurityPolicyManager;
123
		$this->csrfTokenManager = $csrfTokenManager;
124
		$this->cspNonceManager = $cspNonceManager;
125
		$this->appManager = $appManager;
126
		$this->l10n = $l10n;
127
	}
128
129
	/**
130
	 * This runs all the security checks before a method call. The
131
	 * security checks are determined by inspecting the controller method
132
	 * annotations
133
	 * @param Controller $controller the controller
134
	 * @param string $methodName the name of the method
135
	 * @throws SecurityException when a security check fails
136
	 */
137
	public function beforeController($controller, $methodName) {
138
139
		// this will set the current navigation entry of the app, use this only
140
		// for normal HTML requests and not for AJAX requests
141
		$this->navigationManager->setActiveEntry($this->appName);
142
143
		// security checks
144
		$isPublicPage = $this->reflector->hasAnnotation('PublicPage');
145
		if(!$isPublicPage) {
146
			if(!$this->isLoggedIn) {
147
				throw new NotLoggedInException();
148
			}
149
150
			if($this->reflector->hasAnnotation('SubAdminRequired')
151
				&& !$this->isSubAdmin
152
				&& !$this->isAdminUser) {
153
				throw new NotAdminException($this->l10n->t('Logged in user must be an admin or sub admin'));
154
			}
155
			if(!$this->reflector->hasAnnotation('SubAdminRequired')
156
				&& !$this->reflector->hasAnnotation('NoAdminRequired')
157
				&& !$this->isAdminUser) {
158
				throw new NotAdminException($this->l10n->t('Logged in user must be an admin'));
159
			}
160
		}
161
162
		// Check for strict cookie requirement
163
		if($this->reflector->hasAnnotation('StrictCookieRequired') || !$this->reflector->hasAnnotation('NoCSRFRequired')) {
164
			if(!$this->request->passesStrictCookieCheck()) {
165
				throw new StrictCookieMissingException();
166
			}
167
		}
168
		// CSRF check - also registers the CSRF token since the session may be closed later
169
		Util::callRegister();
170
		if(!$this->reflector->hasAnnotation('NoCSRFRequired')) {
171
			/*
172
			 * Only allow the CSRF check to fail on OCS Requests. This kind of
173
			 * hacks around that we have no full token auth in place yet and we
174
			 * do want to offer CSRF checks for web requests.
175
			 *
176
			 * Additionally we allow Bearer authenticated requests to pass on OCS routes.
177
			 * This allows oauth apps (e.g. moodle) to use the OCS endpoints
178
			 */
179
			if(!$this->request->passesCSRFCheck() && !(
180
					$controller instanceof OCSController && (
181
						$this->request->getHeader('OCS-APIREQUEST') === 'true' ||
182
						strpos($this->request->getHeader('Authorization'), 'Bearer ') === 0
183
					)
184
				)) {
185
				throw new CrossSiteRequestForgeryException();
186
			}
187
		}
188
189
		/**
190
		 * Checks if app is enabled (also includes a check whether user is allowed to access the resource)
191
		 * The getAppPath() check is here since components such as settings also use the AppFramework and
192
		 * therefore won't pass this check.
193
		 * If page is public, app does not need to be enabled for current user/visitor
194
		 */
195
		try {
196
			$appPath = $this->appManager->getAppPath($this->appName);
197
		} catch (AppPathNotFoundException $e) {
198
			$appPath = false;
199
		}
200
201
		if ($appPath !== false && !$isPublicPage && !$this->appManager->isEnabledForUser($this->appName)) {
202
			throw new AppNotEnabledException();
203
		}
204
	}
205
206
	/**
207
	 * Performs the default CSP modifications that may be injected by other
208
	 * applications
209
	 *
210
	 * @param Controller $controller
211
	 * @param string $methodName
212
	 * @param Response $response
213
	 * @return Response
214
	 */
215
	public function afterController($controller, $methodName, Response $response): Response {
216
		$policy = !is_null($response->getContentSecurityPolicy()) ? $response->getContentSecurityPolicy() : new ContentSecurityPolicy();
217
218
		if (get_class($policy) === EmptyContentSecurityPolicy::class) {
219
			return $response;
220
		}
221
222
		$defaultPolicy = $this->contentSecurityPolicyManager->getDefaultPolicy();
223
		$defaultPolicy = $this->contentSecurityPolicyManager->mergePolicies($defaultPolicy, $policy);
224
225
		if($this->cspNonceManager->browserSupportsCspV3()) {
226
			$defaultPolicy->useJsNonce($this->csrfTokenManager->getToken()->getEncryptedValue());
227
		}
228
229
		$response->setContentSecurityPolicy($defaultPolicy);
230
231
		return $response;
232
	}
233
234
	/**
235
	 * If an SecurityException is being caught, ajax requests return a JSON error
236
	 * response and non ajax requests redirect to the index
237
	 * @param Controller $controller the controller that is being called
238
	 * @param string $methodName the name of the method that will be called on
239
	 *                           the controller
240
	 * @param \Exception $exception the thrown exception
241
	 * @throws \Exception the passed in exception if it can't handle it
242
	 * @return Response a Response object or null in case that the exception could not be handled
243
	 */
244
	public function afterException($controller, $methodName, \Exception $exception): Response {
245
		if($exception instanceof SecurityException) {
246
			if($exception instanceof StrictCookieMissingException) {
247
				return new RedirectResponse(\OC::$WEBROOT);
248
 			}
249
			if (stripos($this->request->getHeader('Accept'),'html') === false) {
250
				$response = new JSONResponse(
251
					['message' => $exception->getMessage()],
252
					$exception->getCode()
253
				);
254
			} else {
255
				if($exception instanceof NotLoggedInException) {
256
					$params = [];
257
					if (isset($this->request->server['REQUEST_URI'])) {
258
						$params['redirect_url'] = $this->request->server['REQUEST_URI'];
259
					}
260
					$url = $this->urlGenerator->linkToRoute('core.login.showLoginForm', $params);
261
					$response = new RedirectResponse($url);
262
				} else {
263
					$response = new TemplateResponse('core', '403', ['message' => $exception->getMessage()], 'guest');
264
					$response->setStatus($exception->getCode());
265
				}
266
			}
267
268
			$this->logger->logException($exception, [
269
				'level' => ILogger::DEBUG,
270
				'app' => 'core',
271
			]);
272
			return $response;
273
		}
274
275
		throw $exception;
276
	}
277
278
}
279