diff options
Diffstat (limited to '')
5 files changed, 604 insertions, 0 deletions
diff --git a/library/Director/Web/Controller/Extension/CoreApi.php b/library/Director/Web/Controller/Extension/CoreApi.php new file mode 100644 index 0000000..75cba50 --- /dev/null +++ b/library/Director/Web/Controller/Extension/CoreApi.php @@ -0,0 +1,46 @@ +<?php + +namespace Icinga\Module\Director\Web\Controller\Extension; + +use Icinga\Module\Director\Objects\IcingaEndpoint; +use Icinga\Module\Director\Core\CoreApi as Api; + +trait CoreApi +{ + /** @var Api */ + private $api; + + /** + * @return Api|null + */ + public function getApiIfAvailable() + { + if ($this->api === null) { + if ($this->db()->hasDeploymentEndpoint()) { + $endpoint = $this->db()->getDeploymentEndpoint(); + $this->api = $endpoint->api(); + } + } + + return $this->api; + } + + /** + * @param string $endpointName + * @return Api + */ + public function api($endpointName = null) + { + if ($this->api === null) { + if ($endpointName === null) { + $endpoint = $this->db()->getDeploymentEndpoint(); + } else { + $endpoint = IcingaEndpoint::load($endpointName, $this->db()); + } + + $this->api = $endpoint->api(); + } + + return $this->api; + } +} diff --git a/library/Director/Web/Controller/Extension/DirectorDb.php b/library/Director/Web/Controller/Extension/DirectorDb.php new file mode 100644 index 0000000..03bec81 --- /dev/null +++ b/library/Director/Web/Controller/Extension/DirectorDb.php @@ -0,0 +1,160 @@ +<?php + +namespace Icinga\Module\Director\Web\Controller\Extension; + +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Web\Controller\ActionController; +use Icinga\Module\Director\Web\Window; +use RuntimeException; + +trait DirectorDb +{ + /** @var Db */ + private $db; + + protected function getDbResourceName() + { + if ($name = $this->getDbResourceNameFromRequest()) { + return $name; + } elseif ($name = $this->getPreferredDbResourceName()) { + return $name; + } else { + return $this->getFirstDbResourceName(); + } + } + + protected function getDbResourceNameFromRequest() + { + $param = 'dbResourceName'; + // We shouldn't access _POST and _GET. However, this trait is used + // in various places - and our Request is going to be replaced anyways. + // So, let's not over-engineer things, this is quick & dirty: + if (isset($_POST[$param])) { + $name = $_POST[$param]; + } elseif (isset($_GET[$param])) { + $name = $_GET[$param]; + } else { + return null; + } + + if (in_array($name, $this->listAllowedDbResourceNames())) { + return $name; + } else { + return null; + } + } + + protected function getPreferredDbResourceName() + { + return $this->getWindowSessionValue('db_resource'); + } + + protected function getFirstDbResourceName() + { + $names = $this->listAllowedDbResourceNames(); + if (empty($names)) { + return null; + } else { + return array_shift($names); + } + } + + protected function listAllowedDbResourceNames() + { + /** @var \Icinga\Authentication\Auth $auth */ + $auth = $this->Auth(); + + $available = $this->listAvailableDbResourceNames(); + if ($resourceNames = $auth->getRestrictions('director/db_resource')) { + $names = []; + foreach ($resourceNames as $rNames) { + foreach ($this->splitList($rNames) as $name) { + if (array_key_exists($name, $available)) { + $names[] = $name; + } + } + } + + return $names; + } else { + return $available; + } + } + + /** + * @param string $string + * @return array + */ + protected function splitList($string) + { + return preg_split('/\s*,\s*/', $string, -1, PREG_SPLIT_NO_EMPTY); + } + + protected function isMultiDbSetup() + { + return count($this->listAvailableDbResourceNames()) > 1; + } + + /** + * @return array + */ + protected function listAvailableDbResourceNames() + { + /** @var \Icinga\Application\Config $config */ + $config = $this->Config(); + $resources = $config->get('db', 'resources'); + if ($resources === null) { + $resource = $config->get('db', 'resource'); + if ($resource === null) { + return []; + } else { + return [$resource => $resource]; + } + } else { + $resources = $this->splitList($resources); + $resources = array_combine($resources, $resources); + // natsort doesn't work!? + ksort($resources, SORT_NATURAL); + if ($resource = $config->get('db', 'resource')) { + unset($resources[$resource]); + $resources = [$resource => $resource] + $resources; + } + + return $resources; + } + } + + protected function getWindowSessionValue($value, $default = null) + { + /** @var Window $window */ + $window = $this->Window(); + /** @var \Icinga\Web\Session\SessionNamespace $session */ + $session = $window->getSessionNamespace('director'); + + return $session->get($value, $default); + } + + /** + * + * @return Db + */ + public function db() + { + if ($this->db === null) { + $resourceName = $this->getDbResourceName(); + if ($resourceName) { + $this->db = Db::fromResourceName($resourceName); + } elseif ($this instanceof ActionController) { + if ($this->getRequest()->isApiRequest()) { + throw new RuntimeException('Icinga Director is not correctly configured'); + } else { + $this->redirectNow('director'); + } + } else { + throw new RuntimeException('Icinga Director is not correctly configured'); + } + } + + return $this->db; + } +} diff --git a/library/Director/Web/Controller/Extension/ObjectRestrictions.php b/library/Director/Web/Controller/Extension/ObjectRestrictions.php new file mode 100644 index 0000000..bedb3f1 --- /dev/null +++ b/library/Director/Web/Controller/Extension/ObjectRestrictions.php @@ -0,0 +1,48 @@ +<?php + +namespace Icinga\Module\Director\Web\Controller\Extension; + +use Icinga\Authentication\Auth; +use Icinga\Module\Director\Db; +use Icinga\Module\Director\Objects\IcingaObject; +use Icinga\Module\Director\Restriction\HostgroupRestriction; +use Icinga\Module\Director\Restriction\ObjectRestriction; + +trait ObjectRestrictions +{ + /** @var ObjectRestriction[] */ + private $objectRestrictions; + + /** + * @return ObjectRestriction[] + */ + public function getObjectRestrictions() + { + if ($this->objectRestrictions === null) { + $this->objectRestrictions = $this->loadObjectRestrictions($this->db(), $this->Auth()); + } + + return $this->objectRestrictions; + } + + /** + * @return ObjectRestriction[] + */ + protected function loadObjectRestrictions(Db $db, Auth $auth) + { + return [ + new HostgroupRestriction($db, $auth) + ]; + } + + public function allowsObject(IcingaObject $object) + { + foreach ($this->getObjectRestrictions() as $restriction) { + if (! $restriction->allows($object)) { + return false; + } + } + + return true; + } +} diff --git a/library/Director/Web/Controller/Extension/RestApi.php b/library/Director/Web/Controller/Extension/RestApi.php new file mode 100644 index 0000000..3158f49 --- /dev/null +++ b/library/Director/Web/Controller/Extension/RestApi.php @@ -0,0 +1,114 @@ +<?php + +namespace Icinga\Module\Director\Web\Controller\Extension; + +use Icinga\Exception\AuthenticationException; +use Icinga\Exception\NotFoundError; +use Icinga\Module\Director\Exception\JsonException; +use Icinga\Web\Response; +use InvalidArgumentException; +use Zend_Controller_Response_Exception; + +trait RestApi +{ + protected function isApified() + { + if (property_exists($this, 'isApified')) { + return $this->isApified; + } else { + return false; + } + } + + /** + * @return bool + */ + protected function sendNotFoundForRestApi() + { + /** @var \Icinga\Web\Request $request */ + $request = $this->getRequest(); + if ($request->isApiRequest()) { + $this->sendJsonError($this->getResponse(), 'Not found', 404); + return true; + } else { + return false; + } + } + + /** + * @return bool + */ + protected function sendNotFoundUnlessRestApi() + { + /** @var \Icinga\Web\Request $request */ + $request = $this->getRequest(); + if ($request->isApiRequest()) { + return false; + } else { + $this->sendJsonError($this->getResponse(), 'Not found', 404); + return true; + } + } + + /** + * @throws AuthenticationException + */ + protected function assertApiPermission() + { + if (! $this->hasPermission('director/api')) { + throw new AuthenticationException('You are not allowed to access this API'); + } + } + + /** + * @throws AuthenticationException + * @throws NotFoundError + */ + protected function checkForRestApiRequest() + { + /** @var \Icinga\Web\Request $request */ + $request = $this->getRequest(); + if ($request->isApiRequest()) { + $this->assertApiPermission(); + if (! $this->isApified()) { + throw new NotFoundError('No such API endpoint found'); + } + } + } + + /** + * @param Response $response + * @param $object + */ + protected function sendJson(Response $response, $object) + { + $response->setHeader('Content-Type', 'application/json', true); + echo json_encode($object, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n"; + } + + /** + * @param Response $response + * @param string $message + * @param int|null $code + */ + protected function sendJsonError(Response $response, $message, $code = null) + { + if ($code !== null) { + try { + $response->setHttpResponseCode((int) $code); + } catch (Zend_Controller_Response_Exception $e) { + throw new InvalidArgumentException($e->getMessage(), 0, $e); + } + } + + $this->sendJson($response, (object) ['error' => $message]); + } + + /** + * @return string + */ + protected function getLastJsonError() + { + return JsonException::getJsonErrorMessage(json_last_error()); + } +} diff --git a/library/Director/Web/Controller/Extension/SingleObjectApiHandler.php b/library/Director/Web/Controller/Extension/SingleObjectApiHandler.php new file mode 100644 index 0000000..bc51548 --- /dev/null +++ b/library/Director/Web/Controller/Extension/SingleObjectApiHandler.php @@ -0,0 +1,236 @@ +<?php + +namespace Icinga\Module\Director\Web\Controller\Extension; + +use Exception; +use Icinga\Exception\IcingaException; +use Icinga\Exception\InvalidPropertyException; +use Icinga\Exception\NotFoundError; +use Icinga\Module\Director\Forms\IcingaDeleteObjectForm; +use Icinga\Module\Director\Objects\IcingaObject; +use Icinga\Web\Request; +use Icinga\Web\Response; + +class SingleObjectApiHandler +{ + use DirectorDb; + + /** @var IcingaObject */ + private $object; + + /** @var string */ + private $type; + + /** @var Request */ + private $request; + + /** @var Response */ + private $response; + + /** @var \Icinga\Web\UrlParams */ + private $params; + + public function __construct($type, Request $request, Response $response) + { + $this->type = $type; + $this->request = $request; + $this->response = $response; + $this->params = $request->getUrl()->getParams(); + } + + public function runFailSafe() + { + try { + $this->loadObject(); + $this->run(); + } catch (NotFoundError $e) { + $this->sendJsonError($e->getMessage(), 404); + } catch (Exception $e) { + $response = $this->response; + if ($response->getHttpResponseCode() === 200) { + $response->setHttpResponseCode(500); + } + + $this->sendJsonError($e->getMessage()); + } + } + + protected function retrieveObject() + { + $this->requireObject(); + $this->sendJson( + $this->object->toPlainObject( + $this->params->shift('resolved'), + ! $this->params->shift('withNull'), + $this->params->shift('properties') + ) + ); + } + + protected function deleteObject() + { + $this->requireObject(); + $obj = $this->object->toPlainObject(false, true); + $form = new IcingaDeleteObjectForm(); + $form->setObject($this->object) + ->setRequest($this->request) + ->onSuccess(); + + $this->sendJson($obj); + } + + protected function storeObject() + { + $data = json_decode($this->request->getRawBody()); + + if ($data === null) { + $this->response->setHttpResponseCode(400); + throw new IcingaException( + 'Invalid JSON: %s' . $this->request->getRawBody(), + $this->getLastJsonError() + ); + } else { + $data = (array) $data; + } + + if ($object = $this->object) { + if ($this->request->getMethod() === 'POST') { + $object->setProperties($data); + } else { + $data = array_merge([ + 'object_type' => $object->object_type, + 'object_name' => $object->object_name + ], $data); + $object->replaceWith( + IcingaObject::createByType($this->type, $data, $db) + ); + } + } else { + $object = IcingaObject::createByType($this->type, $data, $db); + } + + if ($object->hasBeenModified()) { + $status = $object->hasBeenLoadedFromDb() ? 200 : 201; + $object->store(); + $this->response->setHttpResponseCode($status); + } else { + $this->response->setHttpResponseCode(304); + } + + $this->sendJson($object->toPlainObject(false, true)); + } + + public function run() + { + switch ($this->request->getMethod()) { + case 'DELETE': + $this->deleteObject(); + break; + + case 'POST': + case 'PUT': + $this->storeObject(); + break; + + case 'GET': + $this->retrieveObject(); + break; + + default: + $this->response->setHttpResponseCode(400); + throw new IcingaException( + 'Unsupported method: %s', + $this->request->getMethod() + ); + } + } + + protected function requireObject() + { + if (! $this->object) { + $this->response->setHttpResponseCode(404); + if (! $this->params->get('name')) { + throw new NotFoundError('You need to pass a "name" parameter to access a specific object'); + } else { + throw new NotFoundError('No such object available'); + } + } + } + + // TODO: just return json_last_error_msg() for PHP >= 5.5.0 + protected function getLastJsonError() + { + switch (json_last_error()) { + case JSON_ERROR_DEPTH: + return 'The maximum stack depth has been exceeded'; + case JSON_ERROR_CTRL_CHAR: + return 'Control character error, possibly incorrectly encoded'; + case JSON_ERROR_STATE_MISMATCH: + return 'Invalid or malformed JSON'; + case JSON_ERROR_SYNTAX: + return 'Syntax error'; + case JSON_ERROR_UTF8: + return 'Malformed UTF-8 characters, possibly incorrectly encoded'; + default: + return 'An error occured when parsing a JSON string'; + } + } + + protected function sendJson($object) + { + $this->response->setHeader('Content-Type', 'application/json', true); + $this->_helper->layout()->disableLayout(); + $this->_helper->viewRenderer->setNoRender(true); + echo json_encode($object, JSON_PRETTY_PRINT) . "\n"; + } + + protected function sendJsonError($message, $code = null) + { + $response = $this->response; + + if ($code !== null) { + $response->setHttpResponseCode((int) $code); + } + + $this->sendJson((object) ['error' => $message]); + } + + protected function loadObject() + { + if ($this->object === null) { + if ($name = $this->params->get('name')) { + $this->object = IcingaObject::loadByType( + $this->type, + $name, + $this->db() + ); + + if (! $this->allowsObject($this->object)) { + $this->object = null; + throw new NotFoundError('No such object available'); + } + } elseif ($id = $this->params->get('id')) { + $this->object = IcingaObject::loadByType( + $this->type, + (int) $id, + $this->db() + ); + } elseif ($this->request->isApiRequest()) { + if ($this->request->isGet()) { + $this->response->setHttpResponseCode(422); + + throw new InvalidPropertyException( + 'Cannot load object, missing parameters' + ); + } + } + } + + return $this->object; + } + + protected function allowsObject(IcingaObject $object) + { + return true; + } +} |