From: Marcin Haba Date: Wed, 10 Aug 2022 11:28:51 +0000 (+0200) Subject: baculum: Add query command support, object endpoint and m365 user list endpoint X-Git-Tag: Release-13.0.2~76 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=2c299ca27268e22f08f445db1426fa04be2ce5cf;p=thirdparty%2Fbacula.git baculum: Add query command support, object endpoint and m365 user list endpoint --- diff --git a/gui/baculum/protected/API/Modules/Bconsole.php b/gui/baculum/protected/API/Modules/Bconsole.php index ca15b71b2..872a04544 100644 --- a/gui/baculum/protected/API/Modules/Bconsole.php +++ b/gui/baculum/protected/API/Modules/Bconsole.php @@ -46,7 +46,7 @@ class Bconsole extends APIModule { const PTYPE_CONFIRM_YES_CMD = 3; const PTYPE_CONFIRM_YES_BG_CMD = 4; - const BCONSOLE_COMMAND_PATTERN = "%s%s -c \"%s\" %s 2>&1 <&1 <%s 2>&1 &"; @@ -96,7 +96,8 @@ class Bconsole extends APIModule { '.api', '.status', '.ls', - 'setbandwidth' + 'setbandwidth', + '.query' ); private $config; diff --git a/gui/baculum/protected/API/Modules/ConsoleOutputQueryPage.php b/gui/baculum/protected/API/Modules/ConsoleOutputQueryPage.php new file mode 100644 index 000000000..23fa33e7c --- /dev/null +++ b/gui/baculum/protected/API/Modules/ConsoleOutputQueryPage.php @@ -0,0 +1,50 @@ + + * @category API + * @package Baculum API + */ +abstract class ConsoleOutputQueryPage extends ConsoleOutputPage { + + /** + * Parse '.query' type command output in key=value form. + * + * @param array $output dot query command output + * @return array parsed output + */ + protected function parseOutputKeyValue(array $output) { + $ret = []; + for ($i = 0; $i < count($output); $i++) { + if (preg_match('/(?P\w+)=(?P.+?)$/i', $output[$i], $matches) === 1) { + $ret[$matches['key']] = $matches['value']; + } + } + return $ret; + } +} +?> diff --git a/gui/baculum/protected/API/Modules/ObjectManager.php b/gui/baculum/protected/API/Modules/ObjectManager.php new file mode 100644 index 000000000..26154e4f6 --- /dev/null +++ b/gui/baculum/protected/API/Modules/ObjectManager.php @@ -0,0 +1,56 @@ + + * @category Module + * @package Baculum API + */ +class ObjectManager extends APIModule { + + public function getObjects($criteria = array(), $limit_val = null) { + $sort_col = 'ObjectId'; + $db_params = $this->getModule('api_config')->getConfig('db'); + if ($db_params['type'] === Database::PGSQL_TYPE) { + $sort_col = strtolower($sort_col); + } + $order = ' ORDER BY ' . $sort_col . ' DESC'; + $limit = ''; + if(is_int($limit_val) && $limit_val > 0) { + $limit = ' LIMIT ' . $limit_val; + } + + $where = Database::getWhere($criteria); + + $sql = 'SELECT Object.*, +Job.Name as jobname +FROM Object +LEFT JOIN Job USING (JobId) ' +. $where['where'] . $order . $limit; + + return ObjectRecord::finder()->findAllBySql($sql, $where['params']); + } +} diff --git a/gui/baculum/protected/API/Modules/ObjectRecord.php b/gui/baculum/protected/API/Modules/ObjectRecord.php new file mode 100644 index 000000000..ad45bf423 --- /dev/null +++ b/gui/baculum/protected/API/Modules/ObjectRecord.php @@ -0,0 +1,57 @@ + + * @category Database + * @package Baculum API + */ +class ObjectRecord extends APIDbModule { + + const TABLE = 'Object'; + + public $objectid; + public $jobid; + public $path; + public $filename; + public $pluginname; + public $objecttype; + public $objectname; + public $objectsource; + public $objectuuid; + public $objectsize; + public $objectcategory; + public $objectstatus; + public $objectcount; + + // external properties + public $jobname; + + public static function finder($className = __CLASS__) { + return parent::finder($className); + } +} +?> diff --git a/gui/baculum/protected/API/Modules/PluginConfig.php b/gui/baculum/protected/API/Modules/PluginConfig.php new file mode 100644 index 000000000..5ba969c50 --- /dev/null +++ b/gui/baculum/protected/API/Modules/PluginConfig.php @@ -0,0 +1,47 @@ + + * @category Config + * @package Baculum API + */ +class PluginConfig extends ConfigFileModule { + + /** + * Plugin config directory path. + */ + const CONFIG_DIR_PATH = 'Baculum.API.Config.plugins'; + + /** + * Get plugin base directory for plugins configuration. + */ + protected function getConfigDirPath() { + return self::CONFIG_DIR_PATH; + } +} diff --git a/gui/baculum/protected/API/Modules/PluginFDConfig.php b/gui/baculum/protected/API/Modules/PluginFDConfig.php new file mode 100644 index 000000000..76e77cd80 --- /dev/null +++ b/gui/baculum/protected/API/Modules/PluginFDConfig.php @@ -0,0 +1,72 @@ + + * @category Config + * @package Baculum API + */ +class PluginFDConfig extends PluginConfig { + + /** + * Plugin config file format. + */ + const CONFIG_FILE_FORMAT = 'ini'; + + /** + * Plugin config file extension. + */ + const CONFIG_FILE_EXT = '.conf'; + + private $config; + + /** + * Get plugin client configuration. + * + * @param string $plugin plugin name (ex. 'm365' or 'mysql') + * @param string $client client name + * @param string $section if given, returned is this section from configuration + * @return array plugin client config + */ + public function getConfig($plugin, $client, $section = null) { + $config = []; + if (is_null($this->config)) { + $fn = sprintf('%s_%s', $client, $plugin); + $cfg_dir_path = $this->getConfigDirPath(); + $path = Prado::getPathOfNamespace($cfg_dir_path) . DIRECTORY_SEPARATOR . $fn . self::CONFIG_FILE_EXT; + $this->config = $this->readConfig($path, self::CONFIG_FILE_FORMAT); + } + if (!is_null($section)) { + $config = key_exists($section, $this->config) ? $this->config[$section] : []; + } else { + $config = $this->config; + } + return $config; + } +} diff --git a/gui/baculum/protected/API/Pages/API/Objects.php b/gui/baculum/protected/API/Pages/API/Objects.php new file mode 100644 index 000000000..b8ecc8fb7 --- /dev/null +++ b/gui/baculum/protected/API/Pages/API/Objects.php @@ -0,0 +1,78 @@ + + * @category API + * @package Baculum API + */ +class Objects extends BaculumAPIServer { + + public function get() { + $misc = $this->getModule('misc'); + $limit = $this->Request->contains('limit') ? intval($this->Request['limit']) : 0; + $objecttype = $this->Request->contains('objecttype') && $misc->isValidName($this->Request['objecttype']) ? $this->Request['objecttype'] : null; + $objectname = $this->Request->contains('objectname') && $misc->isValidName($this->Request['objectname']) ? $this->Request['objectname'] : null; + $objectcategory = $this->Request->contains('objectcategory') && $misc->isValidName($this->Request['objectcategory']) ? $this->Request['objectcategory'] : null; + $objectsource = $this->Request->contains('objectsource') && $misc->isValidName($this->Request['objectsource']) ? $this->Request['objectsource'] : null; + $objectuuid = $this->Request->contains('objectuuid') && $misc->isValidName($this->Request['objectuuid']) ? $this->Request['objectuuid'] : null; + $objectstatus = $this->Request->contains('objectstatus') && $misc->isValidState($this->Request['objectstatus']) ? $this->Request['objectstatus'] : null; + $jobname = $this->Request->contains('jobname') && $misc->isValidName($this->Request['jobname']) ? $this->Request['jobname'] : null; + + $params = []; + if (!empty($objecttype)) { + $params['Object.ObjectType']['operator'] = ''; + $params['Object.ObjectType']['vals'] = $objecttype; + } + if (!empty($objectname)) { + $params['Object.ObjectName']['operator'] = ''; + $params['Object.ObjectName']['vals'] = $objectname; + } + if (!empty($objectcategory)) { + $params['Object.ObjectCategory']['operator'] = ''; + $params['Object.ObjectCategory']['vals'] = $objectcategory; + } + if (!empty($objectsource)) { + $params['Object.ObjectSource']['operator'] = ''; + $params['Object.ObjectSource']['vals'] = $objectsource; + } + if (!empty($objectuuid)) { + $params['Object.ObjectUUID']['operator'] = ''; + $params['Object.ObjectUUID']['vals'] = $objectuuid; + } + if (!empty($objectstatus)) { + $params['Object.ObjectStatus']['operator'] = ''; + $params['Object.ObjectStatus']['vals'] = $objectstatus; + } + if (!empty($jobname)) { + $params['Job.Name']['operator'] = ''; + $params['Job.Name']['vals'] = $jobname; + } + $objects = $this->getModule('object')->getObjects($params, $limit); + $this->output = $objects; + $this->error = ObjectError::ERROR_NO_ERRORS; + } +} diff --git a/gui/baculum/protected/API/Pages/API/PluginM365ListLoggedUsers.php b/gui/baculum/protected/API/Pages/API/PluginM365ListLoggedUsers.php new file mode 100644 index 000000000..a2effb8cd --- /dev/null +++ b/gui/baculum/protected/API/Pages/API/PluginM365ListLoggedUsers.php @@ -0,0 +1,194 @@ + + * @category API + * @package Baculum API + */ +class PluginM365ListLoggedUsers extends ConsoleOutputQueryPage { + + public function get() { + $misc = $this->getModule('misc'); + $client = null; + $clientid = $this->Request->contains('id') ? (int)$this->Request['id'] : 0; + $result = $this->getModule('bconsole')->bconsoleCommand( + $this->director, + ['.client'], + null, + true + ); + if ($result->exitcode === 0) { + $client_val = $this->getModule('client')->getClientById($clientid); + if (is_object($client_val) && in_array($client_val->name, $result->output)) { + $client = $client_val->name; + } else { + $this->output = ClientError::MSG_ERROR_CLIENT_DOES_NOT_EXISTS; + $this->error = ClientError::ERROR_CLIENT_DOES_NOT_EXISTS; + return; + } + } else { + $this->output = PluginError::MSG_ERROR_WRONG_EXITCODE; + $this->error = PluginError::ERROR_WRONG_EXITCODE; + return; + } + + $out_format = ConsoleOutputPage::OUTPUT_FORMAT_RAW; + if ($this->Request->contains('output') && $this->isOutputFormatValid($this->Request['output'])) { + $out_format = $this->Request['output']; + } + + $tenantid = $this->Request->contains('tenantid') ? $this->Request['tenantid'] : null; + if (!empty($tenantid) && !$misc->isValidUUID($tenantid)) { + $this->output = PluginM365Error::MSG_ERROR_TENANT_DOES_NOT_EXISTS; + $this->error = PluginM365Error::ERROR_TENANT_DOES_NOT_EXISTS; + return; + } + $cfg = []; + $fd_plugin_cfg = $this->getModule('fd_plugin_cfg')->getConfig('m365', $client, $tenantid); + if (!empty($tenantid) && count($fd_plugin_cfg) > 0) { + $cfg[$tenantid] = $fd_plugin_cfg; + } else { + $cfg = $fd_plugin_cfg; + } + + $output = []; + $error = 0; + foreach ($cfg as $tid => $opts) { + $config_file = key_exists('config_file', $opts) ? $opts['config_file']: ''; + $objectid = key_exists('objectid', $opts) ? $opts['objectid']: ''; + $plugin = 'm365: '; + if (!empty($config_file)) { + // Standalone app model + $plugin .= sprintf(' config_file=\"%s\" ', $config_file); + } elseif (!empty($objectid)) { + // Common app model + $plugin .= sprintf(' objectid=\"%s\" ', $objectid); + } + + $out = new \StdClass; + $out->output = []; + $out->error = BconsoleError::ERROR_NO_ERRORS; + $params = ['client' => $client, 'plugin' => $plugin]; + if ($out_format === ConsoleOutputPage::OUTPUT_FORMAT_RAW) { + $out = $this->getRawOutput($params); + } elseif($out_format === ConsoleOutputPage::OUTPUT_FORMAT_JSON) { + $out = $this->getJSONOutput($params); + } + + if ($out->exitcode !== 0) { + $error = PluginM365Error::ERROR_EXECUTING_PLUGIN_QUERY_COMMAND; + $output = PluginM365Error::MSG_ERROR_EXECUTING_PLUGIN_QUERY_COMMAND . $out->output; + $this->getModule('logging')->log( + Logging::CATEGORY_EXECUTE, + $output . ", Error=$error" + ); + continue; + } + $output[$tid] = $out->output; + } + $this->output = $output; + $this->error = $error; + } + + /** + * Get M365 logged user list output from console in raw format. + * + * @param array $params command parameters + * @return StdClass object with output and exitcode + */ + protected function getRawOutput($params = []) { + $ret = $this->getModule('bconsole')->bconsoleCommand( + $this->director, + [ + '.query', + 'plugin="' . $params['plugin'] . '"', + 'client="' . $params['client'] . '"', + 'parameter="logged-users"' + ] + ); + if ($ret->exitcode === 0) { + $ret->output = iterator_to_array($this->getUserRows($ret->output)); + } else { + $ret->output = []; // don't provide errors to output, only in logs + $this->getModule('logging')->log( + Logging::CATEGORY_EXECUTE, + 'Wrong output from m365 RAW user list: ' . implode(PHP_EOL, $ret->output) + ); + } + return $ret; + } + + /** + * Get show director output in JSON format. + * + * @param array $params command parameters + * @return StdClass object with output and exitcode + */ + protected function getJSONOutput($params = []) { + $ret = $this->getModule('bconsole')->bconsoleCommand( + $this->director, + [ + '.query', + 'plugin="' . $params['plugin'] . '"', + 'client="' . $params['client'] . '"', + 'parameter="json|logged-users"' + ] + ); + if ($ret->exitcode === 0) { + $users = iterator_to_array($this->getUserJSON($ret->output)); + if (count($users) === 1) { + $ret->output = json_decode($users[0]); + } else { + $ret->output = []; // don't provide errors to output, only in logs + $this->getModule('logging')->log( + Logging::CATEGORY_EXECUTE, + 'Wrong output from m365 JSON user list: ' . implode(PHP_EOL, $users) + ); + } + } + return $ret; + } + + private function getUserRows(array $output) { + for ($i = 0; $i < count($output); $i++) { + if (preg_match('/^user=/', $output[$i]) === 1) { + yield $output[$i]; + } + } + } + + private function getUserJSON(array $output) { + for ($i = 0; $i < count($output); $i++) { + if (preg_match('/^=?\[/', $output[$i]) === 1) { + yield ltrim($output[$i], '='); + } + } + } +} diff --git a/gui/baculum/protected/API/Pages/API/config.xml b/gui/baculum/protected/API/Pages/API/config.xml index 683b2e0ce..96abe56cd 100644 --- a/gui/baculum/protected/API/Pages/API/config.xml +++ b/gui/baculum/protected/API/Pages/API/config.xml @@ -28,6 +28,7 @@ + @@ -54,5 +55,7 @@ + + diff --git a/gui/baculum/protected/API/Pages/API/endpoints.xml b/gui/baculum/protected/API/Pages/API/endpoints.xml index 4f507d65d..d60165b90 100644 --- a/gui/baculum/protected/API/Pages/API/endpoints.xml +++ b/gui/baculum/protected/API/Pages/API/endpoints.xml @@ -1,5 +1,5 @@ - + @@ -87,6 +87,8 @@ + + @@ -112,8 +114,11 @@ + + + - + diff --git a/gui/baculum/protected/API/openapi_baculum.json b/gui/baculum/protected/API/openapi_baculum.json index 4df9b3e05..ff29591d9 100644 --- a/gui/baculum/protected/API/openapi_baculum.json +++ b/gui/baculum/protected/API/openapi_baculum.json @@ -122,6 +122,15 @@ }, "AutochangerSlotVolume": { "$ref": "#/definitions/AutochangerSlotVolume" + }, + "Objects": { + "type": "array", + "items": { + "$ref": "#/definitions/Object" + } + }, + "Object": { + "$ref": "#/definitions/Object" } }, "parameters": { @@ -277,6 +286,15 @@ "type": "boolean", "default": 0 } + }, + "TenantId": { + "name": "tenantid", + "in": "query", + "description": "Tenant identifier", + "required": false, + "schema": { + "type": "string" + } } } }, @@ -5982,6 +6000,149 @@ } ] } + }, + "/api/v2/objects": { + "get": { + "tags": ["objects"], + "summary": "Object list", + "description": "Get object list.", + "responses": { + "200": { + "description": "List of objects", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "output": { + "$ref": "#/components/schemas/Objects" + }, + "error": { + "type": "integer", + "description": "Error code", + "enum": [0, 1, 2, 3, 4, 5, 6, 7, 11, 1000] + } + } + } + } + } + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/Limit" + }, + { + "name": "objecttype", + "in": "query", + "required": false, + "description": "Object type, ex. 'm365' or 'PostgreSQL'", + "schema": { + "type": "string" + } + }, + { + "name": "objectname", + "in": "query", + "required": false, + "description": "Object name'", + "schema": { + "type": "string" + } + }, + { + "name": "objectcategory", + "in": "query", + "required": false, + "description": "Object category, ex: 'mailbox'", + "schema": { + "type": "string" + } + }, + { + "name": "objectsource", + "in": "query", + "required": false, + "description": "Object data source", + "schema": { + "type": "string" + } + }, + { + "name": "objectuuid", + "in": "query", + "required": false, + "description": "Object UUID", + "schema": { + "type": "string" + } + }, + { + "name": "objectstatus", + "in": "query", + "required": false, + "description": "Object status ex: 'T', 'e' ...", + "schema": { + "type": "string" + } + }, + { + "name": "jobname", + "in": "query", + "required": false, + "description": "Job name", + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/v2/plugins/m365/{clientid}/users": { + "get": { + "tags": ["plugins"], + "summary": "Microsoft 365 plugin logged in user list", + "description": "Get Microsoft 365 logged in user list.", + "responses": { + "200": { + "description": "List of Microsoft 365 users", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "output": { + "type": "object", + "properties": { + "tenantid": { + "type": "array", + "description": "Microsoft 365 users" + } + } + }, + "error": { + "type": "integer", + "description": "Error code", + "enum": [0, 1, 4, 5, 6, 7, 11, 1000] + } + } + } + } + } + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/ClientId" + }, + { + "$ref": "#/components/parameters/TenantId" + }, + { + "$ref": "#/components/parameters/Output" + } + ] + } } }, "definitions": { @@ -6780,6 +6941,63 @@ "type": "integer" } } + }, + "Object": { + "type": "object", + "properties": { + "objectid": { + "description": "Object identifier", + "type": "integer" + }, + "jobid": { + "description": "Job identifier", + "type": "integer" + }, + "path": { + "description": "Object path, ex: /@m365/n45/users/john@finger/email/", + "type": "string" + }, + "filename": { + "description": "Object file name", + "type": "string" + }, + "pluginname": { + "description": "Plugin definition in form 'plugin_name: param1=value1 param2=value2 ...'", + "type": "string" + }, + "objecttype": { + "description": "Object type, ex. 'm365' or 'PostgreSQL'", + "type": "string" + }, + "objectname": { + "description": "Object name", + "type": "string" + }, + "objectsource": { + "description": "Object data source", + "type": "string" + }, + "objectuuid": { + "description": "Object UUID", + "type": "string" + }, + "objectsize": { + "description": "Object size", + "type": "integer" + }, + "objectcategory": { + "description": "Object category, ex: 'mailbox'", + "type": "string" + }, + "objectstatus": { + "description": "Object status ex: 'T', 'e' ...", + "type": "string" + }, + "objectcount": { + "description": "Object count", + "type": "integer" + } + } } } } diff --git a/gui/baculum/protected/Common/Modules/Errors/ObjectError.php b/gui/baculum/protected/Common/Modules/Errors/ObjectError.php new file mode 100644 index 000000000..afba519ee --- /dev/null +++ b/gui/baculum/protected/Common/Modules/Errors/ObjectError.php @@ -0,0 +1,36 @@ + + * @category Errors + * @package Baculum Common + */ +class ObjectError extends GenericError { + const ERROR_OBJECT_DOES_NOT_EXISTS = 500; + + const MSG_ERROR_OBJECT_DOES_NOT_EXISTS = 'Object does not exist.'; +} diff --git a/gui/baculum/protected/Common/Modules/Errors/PluginError.php b/gui/baculum/protected/Common/Modules/Errors/PluginError.php new file mode 100644 index 000000000..480bf6acb --- /dev/null +++ b/gui/baculum/protected/Common/Modules/Errors/PluginError.php @@ -0,0 +1,36 @@ + + * @category Errors + * @package Baculum Common + */ +class PluginError extends GenericError { + const ERROR_EXECUTING_PLUGIN_QUERY_COMMAND = 150; + + const MSG_ERROR_EXECUTING_PLUGIN_QUERY_COMMAND = 'Error executing plugin query command.'; +} diff --git a/gui/baculum/protected/Common/Modules/Errors/PluginM365Error.php b/gui/baculum/protected/Common/Modules/Errors/PluginM365Error.php new file mode 100644 index 000000000..9b3f1edf0 --- /dev/null +++ b/gui/baculum/protected/Common/Modules/Errors/PluginM365Error.php @@ -0,0 +1,36 @@ + + * @category Errors + * @package Baculum Common + */ +class PluginM365Error extends PluginError { + const ERROR_TENANT_DOES_NOT_EXISTS = 160; + + const MSG_ERROR_TENANT_DOES_NOT_EXISTS = 'Tenant does not exist.'; +} diff --git a/gui/baculum/protected/Common/Modules/Miscellaneous.php b/gui/baculum/protected/Common/Modules/Miscellaneous.php index 6521a08bb..9bebc11c1 100644 --- a/gui/baculum/protected/Common/Modules/Miscellaneous.php +++ b/gui/baculum/protected/Common/Modules/Miscellaneous.php @@ -286,6 +286,10 @@ class Miscellaneous extends TModule { return (preg_match('/^(raw|json)$/', $type) === 1); } + public function isValidUUID($uuid) { + return (preg_match('/^[\w]{8}(-[\w]{4}){3}-[\w]{12}$/', $uuid) === 1); + } + public function escapeCharsToConsole($path) { return preg_replace('/([$])/', '\\\${1}', $path); }