RestAuthenticator.php 7.91 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php

namespace SGalinski\SgRest\Middleware;

/***************************************************************
 *  Copyright notice
 *
 *  (c) sgalinski Internet Services (http://www.sgalinski.de)
 *
 *  All rights reserved
 *
 *  This script is part of the TYPO3 project. The TYPO3 project is
 *  free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  The GNU General Public License can be found at
 *  http://www.gnu.org/copyleft/gpl.html.
 *
 *  This script is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  This copyright notice MUST APPEAR in all copies of the script!
 ***************************************************************/

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use SGalinski\SgRest\Utility\PathUtility;
use TYPO3\CMS\Core\Log\LogLevel;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\HttpUtility;

/**
38
39
40
41
42
 * Authenticates incoming REST requests
 *
 * Class RestAuthenticator
 *
 * @package SGalinski\SgRest\Middleware
43
 */
44
class RestAuthenticator extends AbstractRestMiddleware {
45
46

	/**
47
	 * Process an incoming server request.
48
	 *
49
50
51
	 * Processes an incoming server request in order to produce a response.
	 * If unable to produce the response itself, it may delegate to the provided
	 * request handler to do so.
52
53
54
55
56
57
58
59
60
61
62
63
64
65
	 *
	 * @param ServerRequestInterface $request
	 * @param RequestHandlerInterface $handler
	 * @return ResponseInterface
	 * @throws \Exception
	 */
	public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {

		$queryParams = $request->getQueryParams();

		if (!isset($queryParams[self::argumentNamespace])) {
			return $handler->handle($request);
		}

66
67
68
		$this->requestSegments = GeneralUtility::trimExplode(
			'/', $queryParams[self::argumentNamespace]['request'], TRUE
		);
69
		$this->requestHeaders = $request->getHeaders();
70
71

		// Get class name and the action name of the requested rest controller
72
73
74
75
76
77
78
79
80
		try {
			$this->pathSegments = PathUtility::analyseRequestSegments($this->requestSegments);
		} catch (\RuntimeException $exception) {
			if (in_array($exception->getCode(), $this->reasonableExceptionCodes, TRUE)) {
				return $this->createExceptionJsonResponse($exception->getMessage(), $exception->getCode());
			}
			throw $exception;
		}

81
82
		$apiKey = $this->pathSegments['apiKey'];
		$identifier = $this->pathSegments['identifier'];
83
84
85
86
87
88
89
90
91
92
93
		try {
			// @todo: this currently returns a 500 for a permission question?
			$httpPermissions = $this->registrationService->getHttpPermissionsForEntity(
				$this->pathSegments['entity'], $apiKey
			);
		} catch (\RuntimeException $exception) {
			if (in_array($exception->getCode(), $this->reasonableExceptionCodes, TRUE)) {
				return $this->createExceptionJsonResponse($exception->getMessage(), $exception->getCode());
			}
			throw $exception;
		}
94
95
96
		$this->accessGroup = $this->registrationService->getAccessGroupByApiKey($apiKey);

		$className = $this->getClassName();
97

98
		$httpMethod = $request->getMethod();
99
		$actionName = $this->getCallableActionName($httpMethod);
100
101

		/**
102
		 * when the client requests a bearer token, we don't need to do access checks etc. the user verification is done by the AuthServices
103
		 */
104
		if (!($this->pathSegments['entity'] === 'authentication' && $actionName === 'postGetbearertoken' && $httpMethod === 'POST')) {
105
106
107
108

			$authenticated = $this->authenticationService->verifyRequest($this->requestHeaders);

			if (!$authenticated) {
Fabio Stegmeyer's avatar
Fabio Stegmeyer committed
109
110
111
112
113
114
115
116
117
118
119
120

				if ($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['sg_rest']['enableLogging'] === 1) {
					$this->logger->log(
						LogLevel::ERROR,
						'Auth-token was not provided or was invalid.',
						[
							'requestSegments' => $this->requestSegments,
							'requestHeaders' => $this->requestHeaders
						]
					);
				}

121
				return $this->createExceptionJsonResponse('Auth-token was not provided or was invalid.', 401);
122
123
124
125
126
127
			}

			$apiKey = $this->requestSegments[0];
			$verifiedAccess = $this->authenticationService->verifyUserAccess($apiKey);

			if (!$verifiedAccess) {
Fabio Stegmeyer's avatar
Fabio Stegmeyer committed
128
129
130
131
132
133
134
135
136
137
138
139
140

				if ($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['sg_rest']['enableLogging'] === 1) {
					$this->logger->log(
						LogLevel::ERROR,
						'You tried to access an object where you do not have the necessary permissions.',
						[
							'apiKey' => $apiKey,
							'requestSegments' => $this->requestSegments,
							'requestHeaders' => $this->requestHeaders
						]
					);
				}

141
				return $this->createExceptionJsonResponse(
142
143
144
145
146
					'You tried to access an object where you do not have the necessary permissions.', 403
				);
			}

			if ($httpMethod === 'DELETE' && $this->pathSegments['verb'] !== '' && !$httpPermissions['deleteForVerbs']) {
Fabio Stegmeyer's avatar
Fabio Stegmeyer committed
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163

				if ($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['sg_rest']['enableLogging'] === 1) {
					$this->logger->log(
						LogLevel::ERROR,
						'The DELETE method is not permitted for the given path.',
						[
							'httpMethod' => $httpMethod,
							'identifier' => $identifier,
							'apiKey' => $apiKey,
							'requestSegments' => $this->requestSegments,
							'requestHeaders' => $this->requestHeaders,
							'className' => $className,
							'actionName' => $actionName
						]
					);
				}

Matthias Adrowski's avatar
Matthias Adrowski committed
164
165
166
				return $this->createExceptionJsonResponse(
					'The DELETE method is not permitted for the given path.', 405
				);
167
168
169
170
171
172
173
			}

			if (
				($httpMethod === 'PUT' && $identifier <= 0 && !$httpPermissions['putWithIdentifier']) ||
				($httpMethod === 'PATCH' && $identifier <= 0 && !$httpPermissions['patchWithIdentifier']) ||
				($httpMethod === 'POST' && $identifier > 0 && !$httpPermissions['postWithIdentifier'])
			) {
Fabio Stegmeyer's avatar
Fabio Stegmeyer committed
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190

				if ($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['sg_rest']['enableLogging'] === 1) {
					$this->logger->log(
						LogLevel::ERROR,
						'The ' . $httpMethod . ' method is not permitted for the given path.',
						[
							'httpMethod' => $httpMethod,
							'identifier' => $identifier,
							'apiKey' => $apiKey,
							'requestSegments' => $this->requestSegments,
							'requestHeaders' => $this->requestHeaders,
							'className' => $className,
							'actionName' => $actionName
						]
					);
				}

Matthias Adrowski's avatar
Matthias Adrowski committed
191
192
193
				return $this->createExceptionJsonResponse(
					'The ' . $httpMethod . ' method is not permitted for the given path.', 405
				);
194
195
196
197
198
199
200
			}

			if ($httpMethod === 'POST') {
				HttpUtility::setResponseCode(HttpUtility::HTTP_STATUS_201);
			}

			if (!class_exists($className)) {
Fabio Stegmeyer's avatar
Fabio Stegmeyer committed
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
				if ($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['sg_rest']['enableLogging'] === 1) {
					$this->logger->log(
						LogLevel::ERROR,
						'The requested path does not exist.',
						[
							'httpMethod' => $httpMethod,
							'identifier' => $identifier,
							'apiKey' => $apiKey,
							'requestSegments' => $this->requestSegments,
							'requestHeaders' => $this->requestHeaders,
							'className' => $className,
							'actionName' => $actionName
						]
					);
				}

217
				return $this->createExceptionJsonResponse('The requested path does not exist.', 404);
218
219
220
			}

			if ($actionName !== '' && !method_exists($className, $actionName . 'Action')) {
Fabio Stegmeyer's avatar
Fabio Stegmeyer committed
221
222
				$this->logger->log(
					LogLevel::ERROR,
223
224
225
226
227
228
229
230
231
					'The requested path does not exist.',
					[
						'httpMethod' => $httpMethod,
						'identifier' => $identifier,
						'apiKey' => $apiKey,
						'requestSegments' => $this->requestSegments,
						'requestHeaders' => $this->requestHeaders,
						'className' => $className,
						'actionName' => $actionName
Fabio Stegmeyer's avatar
Fabio Stegmeyer committed
232
					]
233
				);
Fabio Stegmeyer's avatar
Fabio Stegmeyer committed
234

235
				return $this->createExceptionJsonResponse('The requested path does not exist.', 404);
236
237
238
239
			}
		}

		// The request is authenticated, the dispatcher now handles the actual initialization of the rest plugin
240
		return $handler->handle($request);
241
242
	}
}