From: Marcin Haba Date: Tue, 13 Jun 2023 13:20:29 +0000 (+0200) Subject: baculum: New API config ACLs X-Git-Tag: Release-13.0.4~70 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=c8af97b24b4385efe78ac8985df8b81480e423a2;p=thirdparty%2Fbacula.git baculum: New API config ACLs Changes: - Add new POST and DELETE config endpoints - Validate Console roles for each config request - Introduce extended API mode (default disabled) --- diff --git a/gui/baculum/protected/API/Modules/APIConfig.php b/gui/baculum/protected/API/Modules/APIConfig.php index 24090973c..ef47eccc8 100644 --- a/gui/baculum/protected/API/Modules/APIConfig.php +++ b/gui/baculum/protected/API/Modules/APIConfig.php @@ -384,4 +384,19 @@ class APIConfig extends ConfigFileModule { } return $actions; } + + /** + * Check if extended mode is enabled. + * + * @return bool true if extended mode is enabled, false otherwise + */ + public function isExtendedMode() { + $is_emode = false; + $api_config = $this->getConfig('api'); + if (key_exists('extended_mode', $api_config) && $api_config['extended_mode'] === '1') { + // without extended mode all resources are allowed + $is_emode = true; + } + return $is_emode; + } } diff --git a/gui/baculum/protected/API/Modules/BaculaConfigACL.php b/gui/baculum/protected/API/Modules/BaculaConfigACL.php new file mode 100644 index 000000000..f5144b327 --- /dev/null +++ b/gui/baculum/protected/API/Modules/BaculaConfigACL.php @@ -0,0 +1,128 @@ + + * @category Module + * @package Baculum API + */ +class BaculaConfigACL extends APIModule +{ + /** + * Special config ACL action names. + */ + const CONFIG_ACL_ACTIONS = [ + 'READ', + 'CREATE', + 'UPDATE', + 'DELETE' + ]; + /** + * Validate if request command is allowed. + * + * @param string $console_name Director Console name + * @param string $action current action (@see BaculaConfigACL::CONFIG_ACL_ACTIONS) + * @param string $component_type component type (currently not used) + * @param string $resource_type resource type + * @return bool true if request command is allowed, false otherwise + */ + public function validateCommand($user_id, $action, $component_type, $resource_type) + { + $valid = false; + $resource = strtoupper($resource_type); + if ($this->validateAction($action)) { + $command_acls = $this->getCommandACLs($user_id); + for ($i = 0; $i < count($command_acls); $i++) { + if ($command_acls[$i]['action'] === $action && $command_acls[$i]['keyword'] === $resource) { + $valid = true; + break; + } + } + } + return $valid; + } + + /** + * Validate action. + * + * @param string $action action name (ex. 'READ' or 'DELETE') + * @return bool true if action is valid, false otherwise + */ + private function validateAction($action) + { + return in_array($action, self::CONFIG_ACL_ACTIONS); + } + + /** + * Get special commands for restrictions from Director Console resource. + * + * @param $console_name console name + * @return array commands for restrictions or empty array if no command found + */ + private function getCommandACLs($console_name) + { + $command_acls = []; + $bacula_setting = $this->getModule('bacula_setting'); + $config = $bacula_setting->getConfig( + 'dir', + 'Console', + $console_name, + [ + 'apply_jobdefs' => true + ] + ); + if ($config['exitcode'] === 0) { + if (key_exists('CommandAcl', $config['output'])) { + $command_acls = $this->findCommands($config['output']['CommandAcl']); + } + } + return $command_acls; + } + + /** + * Find special command ACLs that defines config restrictions. + * It takes CommandACLs directive value and reads it to find commands. + * + * @param array $commands CommandACLs command list + * @return array restriction commands or empty array if no command found + */ + private function findCommands(array $commands) + { + $command_acls = []; + for ($i = 0; $i < count($commands); $i++) { + // @TODO: Propose using commands in form _ or __ + if (preg_match('/^(?P(READ|CREATE|UPDATE|DELETE))_(?P[A-Z]+)$/', $commands[$i], $match) === 1) { + $command_acls[] = [ + 'keyword' => $match['keyword'], + 'action' => $match['action'] + ]; + } + } + return $command_acls; + } + +} +?> diff --git a/gui/baculum/protected/API/Modules/BaculumAPIServer.php b/gui/baculum/protected/API/Modules/BaculumAPIServer.php index 5e2381e4e..eb914e656 100644 --- a/gui/baculum/protected/API/Modules/BaculumAPIServer.php +++ b/gui/baculum/protected/API/Modules/BaculumAPIServer.php @@ -62,11 +62,9 @@ abstract class BaculumAPIServer extends TPage { protected $director; /** - * Web interface User name that sent request to API. - * Null value means administrator, any other value means normal user - * (non-admin user). + * Basic auth username */ - protected $user; + protected $username; /** * Endpoints available for every authenticated client. @@ -101,6 +99,7 @@ abstract class BaculumAPIServer extends TPage { $is_auth = true; $username = isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : null; if ($username) { + $this->username = $username; $props = $this->getModule('basic_config')->getConfig($username); $this->initAuthParams($props); } diff --git a/gui/baculum/protected/API/Pages/API/Config.php b/gui/baculum/protected/API/Pages/API/Config.php index 0a99a08c6..15e3d2c98 100644 --- a/gui/baculum/protected/API/Pages/API/Config.php +++ b/gui/baculum/protected/API/Pages/API/Config.php @@ -21,6 +21,7 @@ */ use Baculum\API\Modules\BaculumAPIServer; +use Baculum\Common\Modules\Errors\AuthorizationError; use Baculum\Common\Modules\Errors\BaculaConfigError; /** @@ -31,6 +32,7 @@ use Baculum\Common\Modules\Errors\BaculaConfigError; * @package Baculum API */ class Config extends BaculumAPIServer { + public function get() { $misc = $this->getModule('misc'); $component_type = $this->Request->contains('component_type') ? $this->Request['component_type'] : null; @@ -41,10 +43,31 @@ class Config extends BaculumAPIServer { if ($apply_jobdefs) { $opts['apply_jobdefs'] = $apply_jobdefs; } + if (!$this->isResourceAllowed( + 'READ', + $component_type, + $resource_type, + $resource_name + )) { + // Access denied. End. + return; + } - $config = $this->getModule('bacula_setting')->getConfig($component_type, $resource_type, $resource_name, $opts); - $this->output = $config['output']; - $this->error = $config['exitcode']; + // Role valid. Access granted + $config = $this->getModule('bacula_setting')->getConfig( + $component_type, + $resource_type, + $resource_name, + $opts + ); + if ($config['exitcode'] === 0 && count($config['output']) == 0) { + // Config does not exists. Nothing to get. + $this->output = BaculaConfigError::MSG_ERROR_CONFIG_DOES_NOT_EXIST; + $this->error = BaculaConfigError::ERROR_CONFIG_DOES_NOT_EXIST; + } else { + $this->output = $config['output']; + $this->error = $config['exitcode']; + } } public function set($id, $params) { @@ -57,9 +80,10 @@ class Config extends BaculumAPIServer { $config = json_decode($config['config'], true); } } else { - $config = array(); + $config = []; } if (is_null($config)) { + // Invalid config. End. $this->output = BaculaConfigError::MSG_ERROR_CONFIG_VALIDATION_ERROR; $this->error = BaculaConfigError::ERROR_CONFIG_VALIDATION_ERROR; return; @@ -68,16 +92,230 @@ class Config extends BaculumAPIServer { $resource_type = $this->Request->contains('resource_type') ? $this->Request['resource_type'] : null; $resource_name = $this->Request->contains('resource_name') ? $this->Request['resource_name'] : null; - $result = $this->getModule('bacula_setting')->setConfig($config, $component_type, $resource_type, $resource_name); - if ($result['save_result'] === true) { - $this->output = BaculaConfigError::MSG_ERROR_NO_ERRORS; - $this->error = BaculaConfigError::ERROR_NO_ERRORS; - } else if ($result['is_valid'] === false) { - $this->output = BaculaConfigError::MSG_ERROR_CONFIG_VALIDATION_ERROR . print_r($result['result'], true); + if (!$this->isResourceAllowed( + 'UPDATE', + $component_type, + $resource_type, + $resource_name + )) { + // Access denied. End. + return; + } + + $resource_config = []; + if (is_string($component_type) && is_string($resource_type) && is_string($resource_name)) { + // Get existing resource config. + $res = $this->getModule('bacula_setting')->getConfig( + $component_type, + $resource_type, + $resource_name + ); + if ($res['exitcode'] === 0) { + $resource_config = $res['output']; + } + } + + if (is_null($resource_name) || count($resource_config) > 0 || $this->getModule('api_config')->isExtendedMode() === false) { + // Config exists. Update it. + $result = $this->getModule('bacula_setting')->setConfig( + $config, + $component_type, + $resource_type, + $resource_name + ); + if ($result['save_result'] === true) { + $this->output = BaculaConfigError::MSG_ERROR_NO_ERRORS; + $this->error = BaculaConfigError::ERROR_NO_ERRORS; + } else if ($result['is_valid'] === false) { + $this->output = BaculaConfigError::MSG_ERROR_CONFIG_VALIDATION_ERROR . print_r($result['result'], true); + $this->error = BaculaConfigError::ERROR_CONFIG_VALIDATION_ERROR; + } else { + $this->output = BaculaConfigError::MSG_ERROR_WRITE_TO_CONFIG_ERROR . print_r($result['result'], true); + $this->error = BaculaConfigError::ERROR_WRITE_TO_CONFIG_ERROR; + } + } else { + // Config does not exists. Nothing to update. + $this->output = BaculaConfigError::MSG_ERROR_CONFIG_DOES_NOT_EXIST; + $this->error = BaculaConfigError::ERROR_CONFIG_DOES_NOT_EXIST; + } + } + + public function create($params) { + $config = (array)$params; + $config = json_decode($config['config'], true); + if (is_null($config)) { + // Invalid config. End. + $this->output = BaculaConfigError::MSG_ERROR_CONFIG_VALIDATION_ERROR; $this->error = BaculaConfigError::ERROR_CONFIG_VALIDATION_ERROR; + return; + } + + $component_type = $this->Request->contains('component_type') ? $this->Request['component_type'] : null; + $resource_type = $this->Request->contains('resource_type') ? $this->Request['resource_type'] : null; + $resource_name = $this->Request->contains('resource_name') ? $this->Request['resource_name'] : null; + + if (!$this->isResourceAllowed( + 'CREATE', + $component_type, + $resource_type, + $resource_name + )) { + // Access denied. End. + return; + } + + $resource_config = []; + if (is_string($component_type) && is_string($resource_type) && is_string($resource_name)) { + // Get existing resource config. + $res = $this->getModule('bacula_setting')->getConfig( + $component_type, + $resource_type, + $resource_name + ); + if ($res['exitcode'] === 0) { + $resource_config = $res['output']; + } + } + + if (is_null($resource_name) || count($resource_config) == 0) { + // Resource does not exists, so add it to config. + $result = $this->getModule('bacula_setting')->setConfig( + $config, + $component_type, + $resource_type, + $resource_name + ); + if ($result['save_result'] === true) { + $this->output = BaculaConfigError::MSG_ERROR_NO_ERRORS; + $this->error = BaculaConfigError::ERROR_NO_ERRORS; + } else if ($result['is_valid'] === false) { + $this->output = BaculaConfigError::MSG_ERROR_CONFIG_VALIDATION_ERROR . print_r($result['result'], true); + $this->error = BaculaConfigError::ERROR_CONFIG_VALIDATION_ERROR; + } else { + $this->output = BaculaConfigError::MSG_ERROR_WRITE_TO_CONFIG_ERROR . print_r($result['result'], true); + $this->error = BaculaConfigError::ERROR_WRITE_TO_CONFIG_ERROR; + } } else { - $this->output = BaculaConfigError::MSG_ERROR_WRITE_TO_CONFIG_ERROR . print_r($result['result'], true); - $this->error = BaculaConfigError::ERROR_WRITE_TO_CONFIG_ERROR; + // Resource already exists. End. + $this->output = BaculaConfigError::MSG_ERROR_CONFIG_ALREADY_EXISTS; + $this->error = BaculaConfigError::ERROR_CONFIG_ALREADY_EXISTS; + } + } + + public function remove($id) { + $component_type = $this->Request->contains('component_type') ? $this->Request['component_type'] : null; + $resource_type = $this->Request->contains('resource_type') ? $this->Request['resource_type'] : null; + $resource_name = $this->Request->contains('resource_name') ? $this->Request['resource_name'] : null; + + if (!$this->isResourceAllowed( + 'DELETE', + $component_type, + $resource_type, + $resource_name + )) { + // Access denied. End. + return; + } + + $config = []; + if (is_string($component_type) && is_string($resource_type) && is_string($resource_name)) { + $res = $this->getModule('bacula_setting')->getConfig( + $component_type + ); + if ($res['exitcode'] === 0) { + $config = $res['output']; + } + } + $config_len = count($config); + if ($config_len > 0) { + $index_del = -1; + for ($i = 0; $i < $config_len; $i++) { + if (!key_exists($resource_type, $config[$i])) { + // skip other resource types + continue; + } + if ($config[$i][$resource_type]['Name'] === $resource_name) { + $index_del = $i; + break; + } + } + if ($index_del > -1) { + array_splice($config, $index_del, 1); + $result = $this->getModule('bacula_setting')->setConfig( + $config, + $component_type + ); + if ($result['save_result'] === true) { + $this->output = BaculaConfigError::MSG_ERROR_NO_ERRORS; + $this->error = BaculaConfigError::ERROR_NO_ERRORS; + } else if ($result['is_valid'] === false) { + $this->output = BaculaConfigError::MSG_ERROR_CONFIG_VALIDATION_ERROR . print_r($result['result'], true); + $this->error = BaculaConfigError::ERROR_CONFIG_VALIDATION_ERROR; + } else { + $this->output = BaculaConfigError::MSG_ERROR_WRITE_TO_CONFIG_ERROR . print_r($result['result'], true); + $this->error = BaculaConfigError::ERROR_WRITE_TO_CONFIG_ERROR; + } + } else { + $this->output = BaculaConfigError::MSG_ERROR_CONFIG_DOES_NOT_EXIST; + $this->error = BaculaConfigError::ERROR_CONFIG_DOES_NOT_EXIST; + } + } else { + $this->output = BaculaConfigError::MSG_ERROR_CONFIG_DOES_NOT_EXIST; + $this->error = BaculaConfigError::ERROR_CONFIG_DOES_NOT_EXIST; + } + } + + /** + * Access denied error. + * User is not allowed to use given action. + * + * @param string $component_type component type + * @param string $resource_type resource type + * @param string $resource_name resource name + * @return none + */ + private function accessDenied($component_type, $resource_type, $resource_name) + { + $emsg = sprintf( + ' ComponentType: %s, ResourceType: %s, ResourceName: %s', + $component_type, + $resource_type, + $resource_name + ); + $this->output = AuthorizationError::MSG_ERROR_ACCESS_ATTEMPT_TO_NOT_ALLOWED_RESOURCE . $emsg; + $this->error = AuthorizationError::ERROR_ACCESS_ATTEMPT_TO_NOT_ALLOWED_RESOURCE; + } + + /** + * Check if resource is allowed for current user. + * + * @param string $component_type component type + * @param string $resource_type resource type + * @param string $resource_name resource name + * @return bool true if user is allowed, false otherwise + */ + private function isResourceAllowed($action, $component_type, $resource_type, $resource_name) + { + if ($this->getModule('api_config')->isExtendedMode() === false) { + // without extended mode all resources are allowed + return true; + } + $bacula_config_acl = $this->getModule('bacula_config_acl'); + $valid = $bacula_config_acl->validateCommand( + $this->username, + $action, + $component_type, + $resource_type + ); + + if (!$valid) { + // Access denied. End. + $this->accessDenied( + $component_type, + $resource_type, + $resource_name + ); } + return $valid; } } diff --git a/gui/baculum/protected/API/Pages/API/config.xml b/gui/baculum/protected/API/Pages/API/config.xml index a7865a57c..41c931ccc 100644 --- a/gui/baculum/protected/API/Pages/API/config.xml +++ b/gui/baculum/protected/API/Pages/API/config.xml @@ -39,6 +39,7 @@ + diff --git a/gui/baculum/protected/API/openapi_baculum.json b/gui/baculum/protected/API/openapi_baculum.json index 01ca99738..16413e007 100644 --- a/gui/baculum/protected/API/openapi_baculum.json +++ b/gui/baculum/protected/API/openapi_baculum.json @@ -6267,6 +6267,55 @@ } ] }, + "post": { + "tags": ["config"], + "summary": "Create component resource config", + "description": "Create specific component resource config", + "consumes": [ "application/json" ], + "responses": { + "200": { + "description": "Create single resource config", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "output": { + "type": "object", + "description": "Create resource config error message" + }, + "error": { + "type": "integer", + "description": "Error code", + "enum": [0, 1, 2, 3, 4, 5, 6, 7, 11, 80, 81, 82, 83, 84, 90, 91, 92, 93, 94, 95, 1000] + } + } + } + } + } + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/ComponentType" + }, + { + "$ref": "#/components/parameters/ResourceType" + }, + { + "$ref": "#/components/parameters/ResourceName" + }, + { + "name": "config", + "in": "body", + "description": "Config in JSON form to create", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, "put": { "tags": ["config"], "summary": "Set component resource config", @@ -6315,6 +6364,46 @@ } } ] + }, + "delete": { + "tags": ["config"], + "summary": "Delete component resource config", + "description": "Delete specific component resource config", + "consumes": [ "application/json" ], + "responses": { + "200": { + "description": "Delete single resource config", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "output": { + "type": "object", + "description": "Delete resource config error message" + }, + "error": { + "type": "integer", + "description": "Error code", + "enum": [0, 1, 2, 3, 4, 5, 6, 7, 11, 80, 81, 82, 83, 84, 90, 91, 92, 93, 94, 96, 1000] + } + } + } + } + } + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/ComponentType" + }, + { + "$ref": "#/components/parameters/ResourceType" + }, + { + "$ref": "#/components/parameters/ResourceName" + } + ] } }, "/api/v2/devices/{device_name}/load": { diff --git a/gui/baculum/protected/Common/Modules/Errors/BaculaConfigError.php b/gui/baculum/protected/Common/Modules/Errors/BaculaConfigError.php index 3e22416b9..a34b4f744 100644 --- a/gui/baculum/protected/Common/Modules/Errors/BaculaConfigError.php +++ b/gui/baculum/protected/Common/Modules/Errors/BaculaConfigError.php @@ -36,10 +36,14 @@ class BaculaConfigError extends GenericError { const ERROR_CONFIG_NO_JSONTOOL_READY = 92; const ERROR_WRITE_TO_CONFIG_ERROR = 93; const ERROR_CONFIG_VALIDATION_ERROR = 94; + const ERROR_CONFIG_ALREADY_EXISTS = 95; + const ERROR_CONFIG_DOES_NOT_EXIST = 96; const MSG_ERROR_CONFIG_DIR_NOT_WRITABLE = 'Config directory is not writable.'; const MSG_ERROR_UNEXPECTED_BACULA_CONFIG_VALUE = 'Unexpected Bacula config value.'; const MSG_ERROR_CONFIG_NO_JSONTOOL_READY = 'No JSON tool ready.'; const MSG_ERROR_WRITE_TO_CONFIG_ERROR = 'Write to config file error.'; const MSG_ERROR_CONFIG_VALIDATION_ERROR = 'Config validation error.'; + const MSG_ERROR_CONFIG_ALREADY_EXISTS = 'Config already exists.'; + const MSG_ERROR_CONFIG_DOES_NOT_EXIST = 'Config does not exist.'; }