--- /dev/null
+<?php
+/*
+ * Bacula(R) - The Network Backup Solution
+ * Baculum - Bacula web interface
+ *
+ * Copyright (C) 2013-2023 Kern Sibbald
+ *
+ * The main author of Baculum is Marcin Haba.
+ * The original author of Bacula is Kern Sibbald, with contributions
+ * from many others, a complete list can be found in the file AUTHORS.
+ *
+ * You may use this file and others of this release according to the
+ * license defined in the LICENSE file, which includes the Affero General
+ * Public License, v3.0 ("AGPLv3") and some additional permissions and
+ * terms pursuant to its AGPLv3 Section 7.
+ *
+ * This notice must be preserved when any source code is
+ * conveyed and/or propagated.
+ *
+ * Bacula(R) is a registered trademark of Kern Sibbald.
+ */
+
+namespace Baculum\API\Modules;
+
+/**
+ * ACLs for Bacula configuration part.
+ *
+ * @author Marcin Haba <marcin.haba@bacula.pl>
+ * @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 <RESOURCE>_<ACTION> or <PREFIX>_<RESOURCE>_<ACTION>
+ if (preg_match('/^(?P<action>(READ|CREATE|UPDATE|DELETE))_(?P<keyword>[A-Z]+)$/', $commands[$i], $match) === 1) {
+ $command_acls[] = [
+ 'keyword' => $match['keyword'],
+ 'action' => $match['action']
+ ];
+ }
+ }
+ return $command_acls;
+ }
+
+}
+?>
*/
use Baculum\API\Modules\BaculumAPIServer;
+use Baculum\Common\Modules\Errors\AuthorizationError;
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;
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) {
$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;
$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;
}
}
}
]
},
+ "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",
}
}
]
+ },
+ "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": {