]> git.sur5r.net Git - bacula/bacula/blob - gui/baculum/protected/API/Class/BaculumAPIServer.php
baculum: Use api client version and introduce api server version
[bacula/bacula] / gui / baculum / protected / API / Class / BaculumAPIServer.php
1 <?php
2 /*
3  * Bacula(R) - The Network Backup Solution
4  * Baculum   - Bacula web interface
5  *
6  * Copyright (C) 2013-2017 Kern Sibbald
7  *
8  * The main author of Baculum is Marcin Haba.
9  * The original author of Bacula is Kern Sibbald, with contributions
10  * from many others, a complete list can be found in the file AUTHORS.
11  *
12  * You may use this file and others of this release according to the
13  * license defined in the LICENSE file, which includes the Affero General
14  * Public License, v3.0 ("AGPLv3") and some additional permissions and
15  * terms pursuant to its AGPLv3 Section 7.
16  *
17  * This notice must be preserved when any source code is
18  * conveyed and/or propagated.
19  *
20  * Bacula(R) is a registered trademark of Kern Sibbald.
21  */
22  
23 Prado::using('System.Web.UI.TPage');
24 Prado::using('System.Exceptions.TException');
25 Prado::using('Application.Common.Class.Errors');
26 Prado::using('Application.Common.Class.OAuth2');
27 Prado::using('Application.Common.Class.Logging');
28 Prado::using('Application.API.Class.BException');
29 Prado::using('Application.API.Class.APIDbModule');
30 Prado::using('Application.API.Class.Bconsole');
31 Prado::using('Application.API.Class.OAuth2.TokenRecord');
32
33 /**
34  * Abstract module from which inherits each of API module.
35  * The module contains methods that are common for all API pages.
36  *
37  * @author Marcin Haba <marcin.haba@bacula.pl>
38  */
39 abstract class BaculumAPIServer extends TPage {
40
41         /**
42          * API server version (used in HTTP header)
43          */
44         const API_SERVER_VERSION = 0.1;
45
46         /**
47          * Storing output from API commands in numeric array.
48          */
49         protected $output;
50
51         /**
52          * Storing error from API commands as integer value.
53          */
54         protected $error;
55
56         /**
57          * Storing currently used Director name for bconsole commands.
58          */
59         protected $director;
60
61         /**
62          * Web interface User name that sent request to API.
63          * Null value means administrator, any other value means normal user
64          * (non-admin user).
65          */
66         protected $user;
67
68         private $public_endpoints = array('auth', 'token', 'welcome', 'catalog', 'dbsize', 'directors');
69
70         /**
71          * Action methods.
72          */
73
74         // get elements
75         const GET_METHOD = 'GET';
76
77         // create new elemenet
78         const POST_METHOD = 'POST';
79
80         // update elements
81         const PUT_METHOD = 'PUT';
82
83         // delete element
84         const DELETE_METHOD = 'DELETE';
85
86         /**
87          * Get request, login user and do request action.
88          *
89          * @access public
90          * @param mixed $params onInit action params
91          * @return none
92          */
93         public function onInit($params) {
94                 parent::onInit($params);
95                 /*
96                  * Workaround to bug in PHP 5.6 by FastCGI that caused general protection error.
97                  * @TODO: Check on newer PHP if it is already fixed.
98                  */
99                 // @TODO: Move it to API module.
100                 //$db_params = $this->getModule('api_config')->getConfig('db');
101                 //APIDbModule::getAPIDbConnection($db_params);
102
103                 // set Director to bconsole execution
104                 $this->director = $this->Request->contains('director') ? $this->Request['director'] : null;
105
106                 $is_auth = false;
107                 $config = $this->getModule('api_config')->getConfig('api');
108                 Logging::$debug_enabled = (array_key_exists('debug', $config) && $config['debug'] == 1);
109                 $headers = $this->getRequest()->getHeaders(CASE_LOWER);
110                 if (array_key_exists('auth_type', $config) && array_key_exists('authorization', $headers) && preg_match('/^\w+ [\w=]+$/', $headers['authorization']) === 1) {
111                         list($type, $token) = explode(' ', $headers['authorization'], 2);
112                         if ($config['auth_type'] === 'oauth2' && $type === 'Bearer') {
113                                 // deleting expired tokens
114                                 $this->getModule('oauth2_token')->deleteExpiredTokens();
115
116                                 $auth = TokenRecord::findByPk($token);
117                                 if (is_array($auth)) {
118                                         if ($this->isScopeValid($auth['scope'])) {
119                                                 // AUTH OK
120                                                 $is_auth = true;
121                                                 $this->init_auth($auth);
122                                         } else {
123                                                 // Scopes error. Access to not allowed resource
124                                                 header(OAuth2::HEADER_UNAUTHORIZED);
125                                                 $url = $this->getRequest()->getUrl()->getPath();
126                                                 $this->output = AuthorizationError::MSG_ERROR_ACCESS_ATTEMPT_TO_NOT_ALLOWED_RESOURCE .' Endpoint: ' .  $url;
127                                                 $this->error = AuthorizationError::ERROR_ACCESS_ATTEMPT_TO_NOT_ALLOWED_RESOURCE;
128                                                 return;
129
130                                         }
131                                 }
132                         } elseif ($config['auth_type'] === 'basic' && $type === 'Basic') {
133                                 // AUTH OK
134                                 $is_auth = true;
135                         }
136
137                 }
138
139                 if ($is_auth === false) {
140                         // Authorization error.
141                         header(OAuth2::HEADER_UNAUTHORIZED);
142                         $this->output = AuthorizationError::MSG_ERROR_AUTHORIZATION_TO_API_PROBLEM;
143                         $this->error = AuthorizationError::ERROR_AUTHORIZATION_TO_API_PROBLEM;
144                         return;
145                 }
146                 try {
147                         switch($_SERVER['REQUEST_METHOD']) {
148                                 case self::GET_METHOD: {
149                                         $this->get();
150                                         break;
151                                 }
152                                 case self::POST_METHOD: {
153                                         $this->post();
154                                         break;
155                                 }
156                                 case self::PUT_METHOD: {
157                                         $this->put();
158                                         break;
159                                 }
160                                 case self::DELETE_METHOD: {
161                                         $this->delete();
162                                         break;
163                                 }
164                         }
165                 } catch(TException $e) {
166                         $this->getModule('logging')->log(
167                                 __FUNCTION__,
168                                 "Method: {$_SERVER['REQUEST_METHOD']} $e",
169                                 Logging::CATEGORY_APPLICATION,
170                                 __FILE__,
171                                 __LINE__
172                         );
173                         if ($e instanceof BException) {
174                                 $this->output = $e->getErrorMessage();
175                                 $this->error = $e->getErrorCode();
176                         } else {
177                                 $this->output = GenericError::MSG_ERROR_INTERNAL_ERROR . ' ' . $e->getErrorMessage();
178                                 $this->error = GenericError::ERROR_INTERNAL_ERROR;
179                         }
180                 } 
181         }
182
183         /**
184          * Initialize auth parameters.
185          *
186          * @param array $auth token params stored in TokenRecord session
187          * @return none
188          */
189         private function init_auth(array $auth) {
190                 // if client has own bconsole config, assign it here
191                 if (array_key_exists('bconsole_cfg_path', $auth) && !empty($auth['bconsole_cfg_path'])) {
192                         Bconsole::setCfgPath($auth['bconsole_cfg_path'], true);
193                 }
194         }
195
196         /**
197          * Get request result data and pack it in JSON format.
198          * JSON values are: {
199          * "output": (list) output values
200          * "error" : (integer) result exit code (0 - OK, non-zero - error)
201          *
202          * @access private
203          * @return string JSON value with output and error values
204          */
205         private function getOutput() {
206                 $output = array('output' => $this->output, 'error' => $this->error);
207                 $this->setOutputHeaders();
208                 $json = json_encode($output);
209                 return $json;
210         }
211
212         /**
213          * Set output headers to send in response.
214          */
215         private function setOutputHeaders() {
216                 $response = $this->getResponse();
217                 $response->setContentType('application/json');
218                 $response->appendHeader('Baculum-API-Version: ' . strval(self::API_SERVER_VERSION));
219         }
220
221         /**
222          * Return action result which was realized in onInit() method.
223          * On standard output is printed JSON value with request results.
224          *
225          * @access public
226          * @param mixed $params onInit action params
227          * @return none
228          */
229         public function onLoad($params) {
230                 parent::onLoad($params);
231                 echo $this->getOutput();
232         }
233
234         /**
235          * Changing/updating values via API.
236          *
237          * @access private
238          * @return none
239          */
240         private function put() {
241                 $id = $this->Request->contains('id') ? $this->Request['id'] : null;
242
243                 /**
244                  * Check if it is possible to read PUT method data.
245                  * Note that some clients sends data in PUT request as PHP input stream which
246                  * is not possible to read by $_REQUEST data. From this reason, when is
247                  * not possible to ready by superglobal $_REQUEST variable, then is try to
248                  * read PUT data by PHP input stream.
249                  */
250                 if ($this->Request->contains('update') && is_array($this->Request['update']) && count($this->Request['update']) > 0) {
251                         // $_REQUEST available to read
252                         $params = (object)$this->Request['update'];
253                         $this->set($id, $params);
254                 } else {
255                         // no possibility to read data from $_REQUEST. Try to load from input stream.
256                         $inputstr = file_get_contents("php://input");
257
258                         /**
259                          * Read using chunks for case large updates (over 1000 values).
260                          * Otherwise max_input_vars limitation in php.ini can be reached (usually
261                          * set to 1000 variables)
262                          * @see http://php.net/manual/en/info.configuration.php#ini.max-input-vars
263                          */
264                         $chunks = explode('&', $inputstr);
265
266                         $response_data = array();
267                         for($i = 0; $i<count($chunks); $i++) {
268                                 // if chunks would not be used, then here occurs reach max_input_vars limit
269                                 parse_str($chunks[$i], $response_el);
270                                 if (is_array($response_el) && array_key_exists('update', $response_el) && is_array($response_el['update'])) {
271                                         $key = key($response_el['update']);
272                                         $response_data['update'][$key] = $response_el['update'][$key];
273                                 }
274                         }
275                         if (is_array($response_data) && array_key_exists('update', $response_data)) {
276                                 $params = (object)$response_data['update'];
277                                 $this->set($id, $params);
278                         } else {
279                                 /**
280                                  * This case should never occur because it means that there is
281                                  * given nothing to update.
282                                  */
283                                 $params = new stdClass;
284                                 $this->set($id, $params);
285                         }
286                 }
287         }
288
289         /**
290          * Creating new elements.
291          *
292          * @access private
293          * @return none
294          */
295         private function post() {
296                 $params = new stdClass;
297                 if ($this->Request->contains('create') && is_array($this->Request['create']) && count($this->Request['create']) > 0) {
298                         $params = (object)$this->Request['create'];
299                 }
300                 $this->create($params);
301         }
302
303         /**
304          * Deleting element by element ID.
305          *
306          * @access private
307          * @return none
308          */
309         private function delete() {
310                 $id = null;
311                 if ($this->Request->contains('id')) {
312                         $id = intval($this->Request['id']);
313                 }
314                 $this->remove($id);
315         }
316
317         /**
318          * Check if request is allowed to access basing on OAuth2 scope.
319          *
320          * @access private
321          * @param string scopes assigned with token
322          * @return bool true if scope in url and from token are valid, otherwise false
323          */
324         private function isScopeValid($scope) {
325                 $is_valid = false;
326                 $scopes = explode(' ', $scope);
327                 $url = $this->getRequest()->getUrl()->getPath();
328                 $params = explode('/', $url);
329                 if (count($params) >= 3 && $params[1] === 'api') {
330                         if (in_array($params[2], $this->public_endpoints)) {
331                                 $is_valid = true;
332                         } else {
333                                 for ($i = 0; $i < count($scopes); $i++) {
334                                         if ($params[2] === $scopes[$i]) {
335                                                 $is_valid = true;
336                                                 break;
337                                         }
338                                 }
339                         }
340                 }
341                 return $is_valid;
342         }
343
344         /**
345          * Shortcut method for getting application modules instances by
346          * module name.
347          *
348          * @access public
349          * @param string $name application module name
350          * @return object module class instance
351          */
352         public function getModule($name) {
353                 return $this->Application->getModule($name);
354         }
355
356         /**
357          * Get Baculum web client version.
358          *
359          * @return float client version
360          */
361         public function getClientVersion() {
362                 $version = 0;
363                 $headers = $this->getRequest()->getHeaders(CASE_LOWER);
364                 if (array_key_exists('x-baculum-api', $headers)) {
365                         $version = floatval($headers['x-baculum-api']);
366                 }
367                 return $version;
368         }
369 }
370 ?>