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