Passed
Push — master ( ec7e83...51197a )
by Roeland
10:19 queued 12s
created

SecurityMiddleware::afterException()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 32
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 22
nc 6
nop 3
dl 0
loc 32
rs 8.9457
c 0
b 0
f 0
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 IAppManager */
88
	private $appManager;
89
	/** @var IL10N */
90
	private $l10n;
91
92
	public function __construct(IRequest $request,
93
								ControllerMethodReflector $reflector,
94
								INavigationManager $navigationManager,
95
								IURLGenerator $urlGenerator,
96
								ILogger $logger,
97
								string $appName,
98
								bool $isLoggedIn,
99
								bool $isAdminUser,
100
								bool $isSubAdmin,
101
								IAppManager $appManager,
102
								IL10N $l10n
103
	) {
104
		$this->navigationManager = $navigationManager;
105
		$this->request = $request;
106
		$this->reflector = $reflector;
107
		$this->appName = $appName;
108
		$this->urlGenerator = $urlGenerator;
109
		$this->logger = $logger;
110
		$this->isLoggedIn = $isLoggedIn;
111
		$this->isAdminUser = $isAdminUser;
112
		$this->isSubAdmin = $isSubAdmin;
113
		$this->appManager = $appManager;
114
		$this->l10n = $l10n;
115
	}
116
117
	/**
118
	 * This runs all the security checks before a method call. The
119
	 * security checks are determined by inspecting the controller method
120
	 * annotations
121
	 * @param Controller $controller the controller
122
	 * @param string $methodName the name of the method
123
	 * @throws SecurityException when a security check fails
124
	 */
125
	public function beforeController($controller, $methodName) {
126
127
		// this will set the current navigation entry of the app, use this only
128
		// for normal HTML requests and not for AJAX requests
129
		$this->navigationManager->setActiveEntry($this->appName);
130
131
		// security checks
132
		$isPublicPage = $this->reflector->hasAnnotation('PublicPage');
133
		if(!$isPublicPage) {
134
			if(!$this->isLoggedIn) {
135
				throw new NotLoggedInException();
136
			}
137
138
			if($this->reflector->hasAnnotation('SubAdminRequired')
139
				&& !$this->isSubAdmin
140
				&& !$this->isAdminUser) {
141
				throw new NotAdminException($this->l10n->t('Logged in user must be an admin or sub admin'));
142
			}
143
			if(!$this->reflector->hasAnnotation('SubAdminRequired')
144
				&& !$this->reflector->hasAnnotation('NoAdminRequired')
145
				&& !$this->isAdminUser) {
146
				throw new NotAdminException($this->l10n->t('Logged in user must be an admin'));
147
			}
148
		}
149
150
		// Check for strict cookie requirement
151
		if($this->reflector->hasAnnotation('StrictCookieRequired') || !$this->reflector->hasAnnotation('NoCSRFRequired')) {
152
			if(!$this->request->passesStrictCookieCheck()) {
153
				throw new StrictCookieMissingException();
154
			}
155
		}
156
		// CSRF check - also registers the CSRF token since the session may be closed later
157
		Util::callRegister();
158
		if(!$this->reflector->hasAnnotation('NoCSRFRequired')) {
159
			/*
160
			 * Only allow the CSRF check to fail on OCS Requests. This kind of
161
			 * hacks around that we have no full token auth in place yet and we
162
			 * do want to offer CSRF checks for web requests.
163
			 *
164
			 * Additionally we allow Bearer authenticated requests to pass on OCS routes.
165
			 * This allows oauth apps (e.g. moodle) to use the OCS endpoints
166
			 */
167
			if(!$this->request->passesCSRFCheck() && !(
168
					$controller instanceof OCSController && (
169
						$this->request->getHeader('OCS-APIREQUEST') === 'true' ||
170
						strpos($this->request->getHeader('Authorization'), 'Bearer ') === 0
171
					)
172
				)) {
173
				throw new CrossSiteRequestForgeryException();
174
			}
175
		}
176
177
		/**
178
		 * Checks if app is enabled (also includes a check whether user is allowed to access the resource)
179
		 * The getAppPath() check is here since components such as settings also use the AppFramework and
180
		 * therefore won't pass this check.
181
		 * If page is public, app does not need to be enabled for current user/visitor
182
		 */
183
		try {
184
			$appPath = $this->appManager->getAppPath($this->appName);
185
		} catch (AppPathNotFoundException $e) {
186
			$appPath = false;
187
		}
188
189
		if ($appPath !== false && !$isPublicPage && !$this->appManager->isEnabledForUser($this->appName)) {
190
			throw new AppNotEnabledException();
191
		}
192
	}
193
194
	/**
195
	 * If an SecurityException is being caught, ajax requests return a JSON error
196
	 * response and non ajax requests redirect to the index
197
	 * @param Controller $controller the controller that is being called
198
	 * @param string $methodName the name of the method that will be called on
199
	 *                           the controller
200
	 * @param \Exception $exception the thrown exception
201
	 * @throws \Exception the passed in exception if it can't handle it
202
	 * @return Response a Response object or null in case that the exception could not be handled
203
	 */
204
	public function afterException($controller, $methodName, \Exception $exception): Response {
205
		if($exception instanceof SecurityException) {
206
			if($exception instanceof StrictCookieMissingException) {
207
				return new RedirectResponse(\OC::$WEBROOT);
208
 			}
209
			if (stripos($this->request->getHeader('Accept'),'html') === false) {
210
				$response = new JSONResponse(
211
					['message' => $exception->getMessage()],
212
					$exception->getCode()
213
				);
214
			} else {
215
				if($exception instanceof NotLoggedInException) {
216
					$params = [];
217
					if (isset($this->request->server['REQUEST_URI'])) {
218
						$params['redirect_url'] = $this->request->server['REQUEST_URI'];
219
					}
220
					$url = $this->urlGenerator->linkToRoute('core.login.showLoginForm', $params);
221
					$response = new RedirectResponse($url);
222
				} else {
223
					$response = new TemplateResponse('core', '403', ['message' => $exception->getMessage()], 'guest');
224
					$response->setStatus($exception->getCode());
225
				}
226
			}
227
228
			$this->logger->logException($exception, [
229
				'level' => ILogger::DEBUG,
230
				'app' => 'core',
231
			]);
232
			return $response;
233
		}
234
235
		throw $exception;
236
	}
237
238
}
239