diff options
Diffstat (limited to '')
23 files changed, 3980 insertions, 0 deletions
diff --git a/application/controllers/AboutController.php b/application/controllers/AboutController.php new file mode 100644 index 0000000..59e3c20 --- /dev/null +++ b/application/controllers/AboutController.php @@ -0,0 +1,27 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Controllers; + +use Icinga\Application\Icinga; +use Icinga\Application\Version; +use Icinga\Web\Controller; + +class AboutController extends Controller +{ + public function indexAction() + { + $this->view->version = Version::get(); + $this->view->libraries = Icinga::app()->getLibraries(); + $this->view->modules = Icinga::app()->getModuleManager()->getLoadedModules(); + $this->view->title = $this->translate('About'); + $this->view->tabs = $this->getTabs()->add( + 'about', + array( + 'label' => $this->translate('About'), + 'title' => $this->translate('About Icinga Web 2'), + 'url' => 'about' + ) + )->activate('about'); + } +} diff --git a/application/controllers/AccountController.php b/application/controllers/AccountController.php new file mode 100644 index 0000000..f172cfe --- /dev/null +++ b/application/controllers/AccountController.php @@ -0,0 +1,83 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Controllers; + +use Icinga\Application\Config; +use Icinga\Authentication\User\UserBackend; +use Icinga\Data\ConfigObject; +use Icinga\Exception\ConfigurationError; +use Icinga\Forms\Account\ChangePasswordForm; +use Icinga\Forms\PreferenceForm; +use Icinga\User\Preferences\PreferencesStore; +use Icinga\Web\Controller; + +/** + * My Account + */ +class AccountController extends Controller +{ + /** + * {@inheritdoc} + */ + public function init() + { + $this->getTabs() + ->add('account', array( + 'title' => $this->translate('Update your account'), + 'label' => $this->translate('My Account'), + 'url' => 'account' + )) + ->add('navigation', array( + 'title' => $this->translate('List and configure your own navigation items'), + 'label' => $this->translate('Navigation'), + 'url' => 'navigation' + )) + ->add( + 'devices', + array( + 'title' => $this->translate('List of devices you are logged in'), + 'label' => $this->translate('My Devices'), + 'url' => 'my-devices' + ) + ); + } + + /** + * My account + */ + public function indexAction() + { + $config = Config::app()->getSection('global'); + $user = $this->Auth()->getUser(); + if ($user->getAdditional('backend_type') === 'db') { + if ($user->can('user/password-change')) { + try { + $userBackend = UserBackend::create($user->getAdditional('backend_name')); + } catch (ConfigurationError $e) { + $userBackend = null; + } + if ($userBackend !== null) { + $changePasswordForm = new ChangePasswordForm(); + $changePasswordForm + ->setBackend($userBackend) + ->handleRequest(); + $this->view->changePasswordForm = $changePasswordForm; + } + } + } + + $form = new PreferenceForm(); + $form->setPreferences($user->getPreferences()); + if (isset($config->config_resource)) { + $form->setStore(PreferencesStore::create(new ConfigObject(array( + 'resource' => $config->config_resource + )), $user)); + } + $form->handleRequest(); + + $this->view->form = $form; + $this->view->title = $this->translate('My Account'); + $this->getTabs()->activate('account'); + } +} diff --git a/application/controllers/AnnouncementsController.php b/application/controllers/AnnouncementsController.php new file mode 100644 index 0000000..ee7fd4c --- /dev/null +++ b/application/controllers/AnnouncementsController.php @@ -0,0 +1,123 @@ +<?php +/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Controllers; + +use Icinga\Exception\NotFoundError; +use Icinga\Forms\Announcement\AcknowledgeAnnouncementForm; +use Icinga\Forms\Announcement\AnnouncementForm; +use Icinga\Web\Announcement\AnnouncementIniRepository; +use Icinga\Web\Controller; +use Icinga\Web\Url; + +class AnnouncementsController extends Controller +{ + public function init() + { + $this->view->title = $this->translate('Announcements'); + + parent::init(); + } + + /** + * List all announcements + */ + public function indexAction() + { + $this->getTabs()->add( + 'announcements', + array( + 'active' => true, + 'label' => $this->translate('Announcements'), + 'title' => $this->translate('List All Announcements'), + 'url' => Url::fromPath('announcements') + ) + ); + + $announcements = (new AnnouncementIniRepository()) + ->select([ + 'id', + 'author', + 'message', + 'start', + 'end' + ]); + + $sortAndFilterColumns = [ + 'author' => $this->translate('Author'), + 'message' => $this->translate('Message'), + 'start' => $this->translate('Start'), + 'end' => $this->translate('End') + ]; + + $this->setupSortControl($sortAndFilterColumns, $announcements, ['start' => 'desc']); + $this->setupFilterControl($announcements, $sortAndFilterColumns, ['message']); + + $this->view->announcements = $announcements->fetchAll(); + } + + /** + * Create an announcement + */ + public function newAction() + { + $this->assertPermission('application/announcements'); + + $form = $this->prepareForm()->add(); + $form->handleRequest(); + $this->renderForm($form, $this->translate('New Announcement')); + } + + /** + * Update an announcement + */ + public function updateAction() + { + $this->assertPermission('application/announcements'); + + $form = $this->prepareForm()->edit($this->params->getRequired('id')); + try { + $form->handleRequest(); + } catch (NotFoundError $_) { + $this->httpNotFound($this->translate('Announcement not found')); + } + $this->renderForm($form, $this->translate('Update Announcement')); + } + + /** + * Remove an announcement + */ + public function removeAction() + { + $this->assertPermission('application/announcements'); + + $form = $this->prepareForm()->remove($this->params->getRequired('id')); + try { + $form->handleRequest(); + } catch (NotFoundError $_) { + $this->httpNotFound($this->translate('Announcement not found')); + } + $this->renderForm($form, $this->translate('Remove Announcement')); + } + + public function acknowledgeAction() + { + $this->assertHttpMethod('POST'); + $this->getResponse()->setHeader('X-Icinga-Container', 'ignore', true); + $form = new AcknowledgeAnnouncementForm(); + $form->handleRequest(); + } + + /** + * Assert permission admin and return a prepared RepositoryForm + * + * @return AnnouncementForm + */ + protected function prepareForm() + { + $form = new AnnouncementForm(); + return $form + ->setRepository(new AnnouncementIniRepository()) + ->setRedirectUrl(Url::fromPath('announcements')); + } +} diff --git a/application/controllers/ApplicationStateController.php b/application/controllers/ApplicationStateController.php new file mode 100644 index 0000000..b828ca2 --- /dev/null +++ b/application/controllers/ApplicationStateController.php @@ -0,0 +1,95 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Controllers; + +use Icinga\Forms\AcknowledgeApplicationStateMessageForm; +use Icinga\Web\Announcement\AnnouncementCookie; +use Icinga\Web\Announcement\AnnouncementIniRepository; +use Icinga\Web\Controller; +use Icinga\Web\RememberMe; +use Icinga\Web\Session; +use Icinga\Web\Widget; + +/** + * @TODO(el): https://dev.icinga.com/issues/10646 + */ +class ApplicationStateController extends Controller +{ + protected $requiresAuthentication = false; + + protected $autorefreshInterval = 60; + + public function init() + { + $this->_helper->layout->disableLayout(); + $this->_helper->viewRenderer->setNoRender(true); + } + + public function indexAction() + { + if ($this->Auth()->isAuthenticated()) { + if (isset($_COOKIE['icingaweb2-session'])) { + $last = (int) $_COOKIE['icingaweb2-session']; + } else { + $last = 0; + } + $now = time(); + if ($last + 600 < $now) { + Session::getSession()->write(); + $params = session_get_cookie_params(); + setcookie( + 'icingaweb2-session', + $now, + 0, + $params['path'], + $params['domain'], + $params['secure'], + $params['httponly'] + ); + $_COOKIE['icingaweb2-session'] = $now; + } + $announcementCookie = new AnnouncementCookie(); + $announcementRepo = new AnnouncementIniRepository(); + if ($announcementCookie->getEtag() !== $announcementRepo->getEtag()) { + $announcementCookie + ->setEtag($announcementRepo->getEtag()) + ->setNextActive($announcementRepo->findNextActive()); + $this->getResponse()->setCookie($announcementCookie); + $this->getResponse()->setHeader('X-Icinga-Announcements', 'refresh', true); + } else { + $nextActive = $announcementCookie->getNextActive(); + if ($nextActive && $nextActive <= $now) { + $announcementCookie->setNextActive($announcementRepo->findNextActive()); + $this->getResponse()->setCookie($announcementCookie); + $this->getResponse()->setHeader('X-Icinga-Announcements', 'refresh', true); + } + } + } + + RememberMe::removeExpired(); + } + + public function summaryAction() + { + if ($this->Auth()->isAuthenticated()) { + $this->getResponse()->setBody((string) Widget::create('ApplicationStateMessages')); + } + } + + public function acknowledgeMessageAction() + { + if (! $this->Auth()->isAuthenticated()) { + $this->getResponse() + ->setHttpResponseCode(401) + ->sendHeaders(); + exit; + } + + $this->assertHttpMethod('POST'); + + $this->getResponse()->setHeader('X-Icinga-Container', 'ignore', true); + + (new AcknowledgeApplicationStateMessageForm())->handleRequest(); + } +} diff --git a/application/controllers/AuthenticationController.php b/application/controllers/AuthenticationController.php new file mode 100644 index 0000000..752f845 --- /dev/null +++ b/application/controllers/AuthenticationController.php @@ -0,0 +1,127 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Controllers; + +use Icinga\Application\Hook\AuthenticationHook; +use Icinga\Application\Icinga; +use Icinga\Application\Logger; +use Icinga\Common\Database; +use Icinga\Exception\AuthenticationException; +use Icinga\Forms\Authentication\LoginForm; +use Icinga\Web\Controller; +use Icinga\Web\Helper\CookieHelper; +use Icinga\Web\RememberMe; +use Icinga\Web\Url; +use RuntimeException; + +/** + * Application wide controller for authentication + */ +class AuthenticationController extends Controller +{ + use Database; + + /** + * {@inheritdoc} + */ + protected $requiresAuthentication = false; + + /** + * {@inheritdoc} + */ + protected $innerLayout = 'inline'; + + /** + * Log into the application + */ + public function loginAction() + { + $icinga = Icinga::app(); + if (($requiresSetup = $icinga->requiresSetup()) && $icinga->setupTokenExists()) { + $this->redirectNow(Url::fromPath('setup')); + } + $form = new LoginForm(); + + if (RememberMe::hasCookie() && $this->hasDb()) { + $authenticated = false; + try { + $rememberMeOld = RememberMe::fromCookie(); + $authenticated = $rememberMeOld->authenticate(); + if ($authenticated) { + $rememberMe = $rememberMeOld->renew(); + $this->getResponse()->setCookie($rememberMe->getCookie()); + $rememberMe->persist($rememberMeOld->getAesCrypt()->getIV()); + } + } catch (RuntimeException $e) { + Logger::error("Can't authenticate user via remember me cookie: %s", $e->getMessage()); + } catch (AuthenticationException $e) { + Logger::error($e); + } + + if (! $authenticated) { + $this->getResponse()->setCookie(RememberMe::forget()); + } + } + + if ($this->Auth()->isAuthenticated()) { + // Call provided AuthenticationHook(s) when login action is called + // but icinga web user is already authenticated + AuthenticationHook::triggerLogin($this->Auth()->getUser()); + + $redirect = $this->params->get('redirect'); + if ($redirect) { + $redirectUrl = Url::fromPath($redirect, [], $this->getRequest()); + if ($redirectUrl->isExternal()) { + $this->httpBadRequest('nope'); + } + } else { + $redirectUrl = $form->getRedirectUrl(); + } + + $this->redirectNow($redirectUrl); + } + if (! $requiresSetup) { + $cookies = new CookieHelper($this->getRequest()); + if (! $cookies->isSupported()) { + $this + ->getResponse() + ->setBody("Cookies must be enabled to run this application.\n") + ->setHttpResponseCode(403) + ->sendResponse(); + exit; + } + $form->handleRequest(); + } + $this->view->form = $form; + $this->view->defaultTitle = $this->translate('Icinga Web 2 Login'); + $this->view->requiresSetup = $requiresSetup; + } + + /** + * Log out the current user + */ + public function logoutAction() + { + $auth = $this->Auth(); + if (! $auth->isAuthenticated()) { + $this->redirectToLogin(); + } + // Get info whether the user is externally authenticated before removing authorization which destroys the + // session and the user object + $isExternalUser = $auth->getUser()->isExternalUser(); + // Call provided AuthenticationHook(s) when logout action is called + AuthenticationHook::triggerLogout($auth->getUser()); + $auth->removeAuthorization(); + if ($isExternalUser) { + $this->view->layout()->setLayout('external-logout'); + $this->getResponse()->setHttpResponseCode(401); + } else { + if (RememberMe::hasCookie() && $this->hasDb()) { + $this->getResponse()->setCookie(RememberMe::forget()); + } + + $this->redirectToLogin(); + } + } +} diff --git a/application/controllers/ConfigController.php b/application/controllers/ConfigController.php new file mode 100644 index 0000000..671e1a7 --- /dev/null +++ b/application/controllers/ConfigController.php @@ -0,0 +1,518 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Controllers; + +use Exception; +use Icinga\Application\Version; +use InvalidArgumentException; +use Icinga\Application\Config; +use Icinga\Application\Icinga; +use Icinga\Application\Modules\Module; +use Icinga\Data\ResourceFactory; +use Icinga\Exception\ConfigurationError; +use Icinga\Exception\NotFoundError; +use Icinga\Forms\ActionForm; +use Icinga\Forms\Config\GeneralConfigForm; +use Icinga\Forms\Config\ResourceConfigForm; +use Icinga\Forms\Config\UserBackendConfigForm; +use Icinga\Forms\Config\UserBackendReorderForm; +use Icinga\Forms\ConfirmRemovalForm; +use Icinga\Security\SecurityException; +use Icinga\Web\Controller; +use Icinga\Web\Notification; +use Icinga\Web\Url; +use Icinga\Web\Widget; + +/** + * Application and module configuration + */ +class ConfigController extends Controller +{ + /** + * Create and return the tabs to display when showing application configuration + */ + public function createApplicationTabs() + { + $tabs = $this->getTabs(); + if ($this->hasPermission('config/general')) { + $tabs->add('general', array( + 'title' => $this->translate('Adjust the general configuration of Icinga Web 2'), + 'label' => $this->translate('General'), + 'url' => 'config/general', + 'baseTarget' => '_main' + )); + } + if ($this->hasPermission('config/resources')) { + $tabs->add('resource', array( + 'title' => $this->translate('Configure which resources are being utilized by Icinga Web 2'), + 'label' => $this->translate('Resources'), + 'url' => 'config/resource', + 'baseTarget' => '_main' + )); + } + if ($this->hasPermission('config/access-control/users') + || $this->hasPermission('config/access-control/groups') + ) { + $tabs->add('authentication', array( + 'title' => $this->translate('Configure the user and group backends'), + 'label' => $this->translate('Access Control Backends'), + 'url' => 'config/userbackend', + 'baseTarget' => '_main' + )); + } + + return $tabs; + } + + public function devtoolsAction() + { + $this->view->tabs = null; + } + + /** + * Redirect to the general configuration + */ + public function indexAction() + { + if ($this->hasPermission('config/general')) { + $this->redirectNow('config/general'); + } elseif ($this->hasPermission('config/resources')) { + $this->redirectNow('config/resource'); + } elseif ($this->hasPermission('config/access-control/*')) { + $this->redirectNow('config/userbackend'); + } else { + throw new SecurityException('No permission to configure Icinga Web 2'); + } + } + + /** + * General configuration + * + * @throws SecurityException If the user lacks the permission for configuring the general configuration + */ + public function generalAction() + { + $this->assertPermission('config/general'); + $form = new GeneralConfigForm(); + $form->setIniConfig(Config::app()); + $form->setOnSuccess(function (GeneralConfigForm $form) { + $config = Config::app(); + $useStrictCsp = (bool) $config->get('security', 'use_strict_csp', false); + if ($form->onSuccess() === false) { + return false; + } + + $appConfigForm = $form->getSubForm('form_config_general_application'); + if ($appConfigForm && (bool) $appConfigForm->getValue('security_use_strict_csp') !== $useStrictCsp) { + $this->getResponse()->setReloadWindow(true); + } + })->handleRequest(); + + $this->view->form = $form; + $this->view->title = $this->translate('General'); + $this->createApplicationTabs()->activate('general'); + } + + /** + * Display the list of all modules + */ + public function modulesAction() + { + $this->assertPermission('config/modules'); + // Overwrite tabs created in init + // @TODO(el): This seems not natural to me. Module configuration should have its own controller. + $this->view->tabs = Widget::create('tabs') + ->add('modules', array( + 'label' => $this->translate('Modules'), + 'title' => $this->translate('List intalled modules'), + 'url' => 'config/modules' + )) + ->activate('modules'); + $this->view->modules = Icinga::app()->getModuleManager()->select() + ->from('modules') + ->order('enabled', 'desc') + ->order('installed', 'asc') + ->order('name'); + $this->setupLimitControl(); + $this->setupPaginationControl($this->view->modules); + $this->view->title = $this->translate('Modules'); + } + + public function moduleAction() + { + $this->assertPermission('config/modules'); + $app = Icinga::app(); + $manager = $app->getModuleManager(); + $name = $this->getParam('name'); + if ($manager->hasInstalled($name) || $manager->hasEnabled($name)) { + $this->view->moduleData = $manager->select()->from('modules')->where('name', $name)->fetchRow(); + if ($manager->hasLoaded($name)) { + $module = $manager->getModule($name); + } else { + $module = new Module($app, $name, $manager->getModuleDir($name)); + } + + $toggleForm = new ActionForm(); + $toggleForm->setDefaults(['identifier' => $name]); + if (! $this->view->moduleData->enabled) { + $toggleForm->setAction(Url::fromPath('config/moduleenable')); + $toggleForm->setDescription(sprintf($this->translate('Enable the %s module'), $name)); + } elseif ($this->view->moduleData->loaded) { + $toggleForm->setAction(Url::fromPath('config/moduledisable')); + $toggleForm->setDescription(sprintf($this->translate('Disable the %s module'), $name)); + } else { + $toggleForm = null; + } + + $this->view->module = $module; + $this->view->libraries = $app->getLibraries(); + $this->view->moduleManager = $manager; + $this->view->toggleForm = $toggleForm; + $this->view->title = $module->getName(); + $this->view->tabs = $module->getConfigTabs()->activate('info'); + $this->view->moduleGitCommitId = Version::getGitHead($module->getBaseDir()); + } else { + $this->view->module = false; + $this->view->tabs = null; + } + } + + /** + * Enable a specific module provided by the 'name' param + */ + public function moduleenableAction() + { + $this->assertPermission('config/modules'); + + $form = new ActionForm(); + $form->setOnSuccess(function (ActionForm $form) { + $moduleName = $form->getValue('identifier'); + $module = Icinga::app()->getModuleManager() + ->enableModule($moduleName) + ->getModule($moduleName); + Notification::success(sprintf($this->translate('Module "%s" enabled'), $moduleName)); + $form->onSuccess(); + + if ($module->hasJs()) { + $this->getResponse() + ->setReloadWindow(true) + ->sendResponse(); + } else { + if ($module->hasCss()) { + $this->reloadCss(); + } + + $this->rerenderLayout()->redirectNow('config/modules'); + } + }); + + try { + $form->handleRequest(); + } catch (Exception $e) { + $this->view->exceptionMessage = $e->getMessage(); + $this->view->moduleName = $form->getValue('identifier'); + $this->view->action = 'enable'; + $this->render('module-configuration-error'); + } + } + + /** + * Disable a module specific module provided by the 'name' param + */ + public function moduledisableAction() + { + $this->assertPermission('config/modules'); + + $form = new ActionForm(); + $form->setOnSuccess(function (ActionForm $form) { + $mm = Icinga::app()->getModuleManager(); + $moduleName = $form->getValue('identifier'); + $module = $mm->getModule($moduleName); + $mm->disableModule($moduleName); + Notification::success(sprintf($this->translate('Module "%s" disabled'), $moduleName)); + $form->onSuccess(); + + if ($module->hasJs()) { + $this->getResponse() + ->setReloadWindow(true) + ->sendResponse(); + } else { + if ($module->hasCss()) { + $this->reloadCss(); + } + + $this->rerenderLayout()->redirectNow('config/modules'); + } + }); + + try { + $form->handleRequest(); + } catch (Exception $e) { + $this->view->exceptionMessage = $e->getMessage(); + $this->view->moduleName = $form->getValue('identifier'); + $this->view->action = 'disable'; + $this->render('module-configuration-error'); + } + } + + /** + * Action for listing user and group backends + */ + public function userbackendAction() + { + if ($this->hasPermission('config/access-control/users')) { + $form = new UserBackendReorderForm(); + $form->setIniConfig(Config::app('authentication')); + $form->handleRequest(); + $this->view->form = $form; + } + + if ($this->hasPermission('config/access-control/groups')) { + $this->view->backendNames = Config::app('groups'); + } + + $this->createApplicationTabs()->activate('authentication'); + $this->view->title = $this->translate('Authentication'); + $this->render('userbackend/reorder'); + } + + /** + * Create a new user backend + */ + public function createuserbackendAction() + { + $this->assertPermission('config/access-control/users'); + $form = new UserBackendConfigForm(); + $form + ->setRedirectUrl('config/userbackend') + ->addDescription($this->translate( + 'Create a new backend for authenticating your users. This backend' + . ' will be added at the end of your authentication order.' + )) + ->setIniConfig(Config::app('authentication')); + + try { + $form->setResourceConfig(ResourceFactory::getResourceConfigs()); + } catch (ConfigurationError $e) { + if ($this->hasPermission('config/resources')) { + Notification::error($e->getMessage()); + $this->redirectNow('config/createresource'); + } + + throw $e; // No permission for resource configuration, show the error + } + + $form->setOnSuccess(function (UserBackendConfigForm $form) { + try { + $form->add($form::transformEmptyValuesToNull($form->getValues())); + } catch (Exception $e) { + $form->error($e->getMessage()); + return false; + } + + if ($form->save()) { + Notification::success(t('User backend successfully created')); + return true; + } + + return false; + }); + $form->handleRequest(); + + $this->view->title = $this->translate('Authentication'); + $this->renderForm($form, $this->translate('New User Backend')); + } + + /** + * Edit a user backend + */ + public function edituserbackendAction() + { + $this->assertPermission('config/access-control/users'); + $backendName = $this->params->getRequired('backend'); + + $form = new UserBackendConfigForm(); + $form->setRedirectUrl('config/userbackend'); + $form->setIniConfig(Config::app('authentication')); + $form->setOnSuccess(function (UserBackendConfigForm $form) use ($backendName) { + try { + $form->edit($backendName, $form::transformEmptyValuesToNull($form->getValues())); + } catch (Exception $e) { + $form->error($e->getMessage()); + return false; + } + + if ($form->save()) { + Notification::success(sprintf(t('User backend "%s" successfully updated'), $backendName)); + return true; + } + + return false; + }); + + try { + $form->load($backendName); + $form->setResourceConfig(ResourceFactory::getResourceConfigs()); + $form->handleRequest(); + } catch (NotFoundError $_) { + $this->httpNotFound(sprintf($this->translate('User backend "%s" not found'), $backendName)); + } + + $this->view->title = $this->translate('Authentication'); + $this->renderForm($form, $this->translate('Update User Backend')); + } + + /** + * Display a confirmation form to remove the backend identified by the 'backend' parameter + */ + public function removeuserbackendAction() + { + $this->assertPermission('config/access-control/users'); + $backendName = $this->params->getRequired('backend'); + + $backendForm = new UserBackendConfigForm(); + $backendForm->setIniConfig(Config::app('authentication')); + $form = new ConfirmRemovalForm(); + $form->setRedirectUrl('config/userbackend'); + $form->setOnSuccess(function (ConfirmRemovalForm $form) use ($backendName, $backendForm) { + try { + $backendForm->delete($backendName); + } catch (Exception $e) { + $form->error($e->getMessage()); + return false; + } + + if ($backendForm->save()) { + Notification::success(sprintf(t('User backend "%s" successfully removed'), $backendName)); + return true; + } + + return false; + }); + $form->handleRequest(); + + $this->view->title = $this->translate('Authentication'); + $this->renderForm($form, $this->translate('Remove User Backend')); + } + + /** + * Display all available resources and a link to create a new one and to remove existing ones + */ + public function resourceAction() + { + $this->assertPermission('config/resources'); + $this->view->resources = Config::app('resources', true)->getConfigObject() + ->setKeyColumn('name') + ->select() + ->order('name'); + $this->view->title = $this->translate('Resources'); + $this->createApplicationTabs()->activate('resource'); + } + + /** + * Display a form to create a new resource + */ + public function createresourceAction() + { + $this->assertPermission('config/resources'); + $this->getTabs()->add('resources/new', array( + 'label' => $this->translate('New Resource'), + 'url' => Url::fromRequest() + ))->activate('resources/new'); + $form = new ResourceConfigForm(); + $form->addDescription($this->translate('Resources are entities that provide data to Icinga Web 2.')); + $form->setIniConfig(Config::app('resources')); + $form->setRedirectUrl('config/resource'); + $form->handleRequest(); + + $this->view->form = $form; + $this->view->title = $this->translate('Resources'); + $this->render('resource/create'); + } + + /** + * Display a form to edit a existing resource + */ + public function editresourceAction() + { + $this->assertPermission('config/resources'); + $this->getTabs()->add('resources/update', array( + 'label' => $this->translate('Update Resource'), + 'url' => Url::fromRequest() + ))->activate('resources/update'); + $form = new ResourceConfigForm(); + $form->setIniConfig(Config::app('resources')); + $form->setRedirectUrl('config/resource'); + $form->handleRequest(); + + $this->view->form = $form; + $this->view->title = $this->translate('Resources'); + $this->render('resource/modify'); + } + + /** + * Display a confirmation form to remove a resource + */ + public function removeresourceAction() + { + $this->assertPermission('config/resources'); + $this->getTabs()->add('resources/remove', array( + 'label' => $this->translate('Remove Resource'), + 'url' => Url::fromRequest() + ))->activate('resources/remove'); + $form = new ConfirmRemovalForm(array( + 'onSuccess' => function ($form) { + $configForm = new ResourceConfigForm(); + $configForm->setIniConfig(Config::app('resources')); + $resource = $form->getRequest()->getQuery('resource'); + + try { + $configForm->remove($resource); + } catch (InvalidArgumentException $e) { + Notification::error($e->getMessage()); + return false; + } + + if ($configForm->save()) { + Notification::success(sprintf(t('Resource "%s" has been successfully removed'), $resource)); + } else { + return false; + } + } + )); + $form->setRedirectUrl('config/resource'); + $form->handleRequest(); + + // Check if selected resource is currently used for authentication + $resource = $this->getRequest()->getQuery('resource'); + $authConfig = Config::app('authentication'); + foreach ($authConfig as $backendName => $config) { + if ($config->get('resource') === $resource) { + $form->warning(sprintf( + $this->translate( + 'The resource "%s" is currently utilized for authentication by user backend "%s".' + . ' Removing the resource can result in noone being able to log in any longer.' + ), + $resource, + $backendName + )); + } + } + + // Check if selected resource is currently used as user preferences backend + if (Config::app()->get('global', 'config_resource') === $resource) { + $form->warning(sprintf( + $this->translate( + 'The resource "%s" is currently utilized to store user preferences. Removing the' + . ' resource causes all current user preferences not being available any longer.' + ), + $resource + )); + } + + $this->view->form = $form; + $this->view->title = $this->translate('Resources'); + $this->render('resource/remove'); + } +} diff --git a/application/controllers/DashboardController.php b/application/controllers/DashboardController.php new file mode 100644 index 0000000..ff2580c --- /dev/null +++ b/application/controllers/DashboardController.php @@ -0,0 +1,346 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Controllers; + +use Exception; +use Zend_Controller_Action_Exception; +use Icinga\Exception\ProgrammingError; +use Icinga\Exception\Http\HttpNotFoundException; +use Icinga\Forms\ConfirmRemovalForm; +use Icinga\Forms\Dashboard\DashletForm; +use Icinga\Web\Controller\ActionController; +use Icinga\Web\Form; +use Icinga\Web\Notification; +use Icinga\Web\Url; +use Icinga\Web\Widget\Dashboard; +use Icinga\Web\Widget\Tabextension\DashboardSettings; + +/** + * Handle creation, removal and displaying of dashboards, panes and dashlets + * + * @see Icinga\Web\Widget\Dashboard for more information about dashboards + */ +class DashboardController extends ActionController +{ + /** + * @var Dashboard; + */ + private $dashboard; + + public function init() + { + $this->dashboard = new Dashboard(); + $this->dashboard->setUser($this->Auth()->getUser()); + $this->dashboard->load(); + } + + public function newDashletAction() + { + $form = new DashletForm(); + $this->getTabs()->add('new-dashlet', array( + 'active' => true, + 'label' => $this->translate('New Dashlet'), + 'url' => Url::fromRequest() + )); + $dashboard = $this->dashboard; + $form->setDashboard($dashboard); + if ($this->_request->getParam('url')) { + $params = $this->_request->getParams(); + $params['url'] = rawurldecode($this->_request->getParam('url')); + $form->populate($params); + } + $action = $this; + $form->setOnSuccess(function (Form $form) use ($dashboard, $action) { + try { + $pane = $dashboard->getPane($form->getValue('pane')); + } catch (ProgrammingError $e) { + $pane = new Dashboard\Pane($form->getValue('pane')); + $pane->setUserWidget(); + $dashboard->addPane($pane); + } + $dashlet = new Dashboard\Dashlet($form->getValue('dashlet'), $form->getValue('url'), $pane); + $dashlet->setUserWidget(); + $pane->addDashlet($dashlet); + $dashboardConfig = $dashboard->getConfig(); + try { + $dashboardConfig->saveIni(); + } catch (Exception $e) { + $action->view->error = $e; + $action->view->config = $dashboardConfig; + $action->render('error'); + return false; + } + Notification::success(t('Dashlet created')); + return true; + }); + $form->setTitle($this->translate('Add Dashlet To Dashboard')); + $form->setRedirectUrl('dashboard'); + $form->handleRequest(); + $this->view->form = $form; + } + + public function updateDashletAction() + { + $this->getTabs()->add('update-dashlet', array( + 'active' => true, + 'label' => $this->translate('Update Dashlet'), + 'url' => Url::fromRequest() + )); + $dashboard = $this->dashboard; + $form = new DashletForm(); + $form->setDashboard($dashboard); + $form->setSubmitLabel($this->translate('Update Dashlet')); + if (! $this->_request->getParam('pane')) { + throw new Zend_Controller_Action_Exception( + 'Missing parameter "pane"', + 400 + ); + } + if (! $this->_request->getParam('dashlet')) { + throw new Zend_Controller_Action_Exception( + 'Missing parameter "dashlet"', + 400 + ); + } + $action = $this; + $form->setOnSuccess(function (Form $form) use ($dashboard, $action) { + try { + $pane = $dashboard->getPane($form->getValue('org_pane')); + $pane->setTitle($form->getValue('pane')); + } catch (ProgrammingError $e) { + $pane = new Dashboard\Pane($form->getValue('pane')); + $pane->setUserWidget(); + $dashboard->addPane($pane); + } + try { + $dashlet = $pane->getDashlet($form->getValue('org_dashlet')); + $dashlet->setTitle($form->getValue('dashlet')); + $dashlet->setUrl($form->getValue('url')); + } catch (ProgrammingError $e) { + $dashlet = new Dashboard\Dashlet($form->getValue('dashlet'), $form->getValue('url'), $pane); + $pane->addDashlet($dashlet); + } + $dashlet->setUserWidget(); + $dashboardConfig = $dashboard->getConfig(); + try { + $dashboardConfig->saveIni(); + } catch (Exception $e) { + $action->view->error = $e; + $action->view->config = $dashboardConfig; + $action->render('error'); + return false; + } + Notification::success(t('Dashlet updated')); + return true; + }); + $form->setTitle($this->translate('Edit Dashlet')); + $form->setRedirectUrl('dashboard/settings'); + $form->handleRequest(); + $pane = $dashboard->getPane($this->getParam('pane')); + $dashlet = $pane->getDashlet($this->getParam('dashlet')); + $form->load($dashlet); + + $this->view->form = $form; + } + + public function removeDashletAction() + { + $form = new ConfirmRemovalForm(); + $this->getTabs()->add('remove-dashlet', array( + 'active' => true, + 'label' => $this->translate('Remove Dashlet'), + 'url' => Url::fromRequest() + )); + $dashboard = $this->dashboard; + if (! $this->_request->getParam('pane')) { + throw new Zend_Controller_Action_Exception( + 'Missing parameter "pane"', + 400 + ); + } + if (! $this->_request->getParam('dashlet')) { + throw new Zend_Controller_Action_Exception( + 'Missing parameter "dashlet"', + 400 + ); + } + $pane = $this->_request->getParam('pane'); + $dashlet = $this->_request->getParam('dashlet'); + $action = $this; + $form->setOnSuccess(function (Form $form) use ($dashboard, $dashlet, $pane, $action) { + $pane = $dashboard->getPane($pane); + $pane->removeDashlet($dashlet); + $dashboardConfig = $dashboard->getConfig(); + try { + $dashboardConfig->saveIni(); + Notification::success(t('Dashlet has been removed from') . ' ' . $pane->getTitle()); + } catch (Exception $e) { + $action->view->error = $e; + $action->view->config = $dashboardConfig; + $action->render('error'); + return false; + } + return true; + }); + $form->setTitle($this->translate('Remove Dashlet From Dashboard')); + $form->setRedirectUrl('dashboard/settings'); + $form->handleRequest(); + $this->view->pane = $pane; + $this->view->dashlet = $dashlet; + $this->view->form = $form; + } + + public function renamePaneAction() + { + $paneName = $this->params->getRequired('pane'); + if (! $this->dashboard->hasPane($paneName)) { + throw new HttpNotFoundException('Pane not found'); + } + + $form = new Form(); + $form->setRedirectUrl('dashboard/settings'); + $form->setSubmitLabel($this->translate('Update Pane')); + $form->addElement( + 'text', + 'name', + array( + 'required' => true, + 'label' => $this->translate('Name') + ) + ); + $form->addElement( + 'text', + 'title', + array( + 'required' => true, + 'label' => $this->translate('Title') + ) + ); + $form->setDefaults(array( + 'name' => $paneName, + 'title' => $this->dashboard->getPane($paneName)->getTitle() + )); + $form->setOnSuccess(function ($form) use ($paneName) { + $newName = $form->getValue('name'); + $newTitle = $form->getValue('title'); + + $pane = $this->dashboard->getPane($paneName); + $pane->setName($newName); + $pane->setTitle($newTitle); + $this->dashboard->getConfig()->saveIni(); + + Notification::success( + sprintf($this->translate('Pane "%s" successfully renamed to "%s"'), $paneName, $newName) + ); + }); + + $form->handleRequest(); + + $this->view->form = $form; + $this->getTabs()->add( + 'update-pane', + array( + 'title' => $this->translate('Update Pane'), + 'url' => $this->getRequest()->getUrl() + ) + )->activate('update-pane'); + } + + public function removePaneAction() + { + $form = new ConfirmRemovalForm(); + $this->createTabs(); + $dashboard = $this->dashboard; + if (! $this->_request->getParam('pane')) { + throw new Zend_Controller_Action_Exception( + 'Missing parameter "pane"', + 400 + ); + } + $pane = $this->_request->getParam('pane'); + $action = $this; + $form->setOnSuccess(function (Form $form) use ($dashboard, $pane, $action) { + $pane = $dashboard->getPane($pane); + $dashboard->removePane($pane->getName()); + $dashboardConfig = $dashboard->getConfig(); + try { + $dashboardConfig->saveIni(); + Notification::success(t('Dashboard has been removed') . ': ' . $pane->getTitle()); + } catch (Exception $e) { + $action->view->error = $e; + $action->view->config = $dashboardConfig; + $action->render('error'); + return false; + } + return true; + }); + $form->setTitle($this->translate('Remove Dashboard')); + $form->setRedirectUrl('dashboard/settings'); + $form->handleRequest(); + $this->view->pane = $pane; + $this->view->form = $form; + } + + /** + * Display the dashboard with the pane set in the 'pane' request parameter + * + * If no pane is submitted or the submitted one doesn't exist, the default pane is + * displayed (normally the first one) + */ + public function indexAction() + { + $this->createTabs(); + if (! $this->dashboard->hasPanes()) { + $this->view->title = 'Dashboard'; + } else { + $panes = array_filter( + $this->dashboard->getPanes(), + function ($pane) { + return ! $pane->getDisabled(); + } + ); + if (empty($panes)) { + $this->view->title = 'Dashboard'; + $this->getTabs()->add('dashboard', array( + 'active' => true, + 'title' => $this->translate('Dashboard'), + 'url' => Url::fromRequest() + )); + } else { + if ($this->_getParam('pane')) { + $pane = $this->_getParam('pane'); + $this->dashboard->activate($pane); + } + if ($this->dashboard === null) { + $this->view->title = 'Dashboard'; + } else { + $this->view->title = $this->dashboard->getActivePane()->getTitle() . ' :: Dashboard'; + if ($this->hasParam('remove')) { + $this->dashboard->getActivePane()->removeDashlet($this->getParam('remove')); + $this->dashboard->getConfig()->saveIni(); + $this->redirectNow(URL::fromRequest()->remove('remove')); + } + $this->view->dashboard = $this->dashboard; + } + } + } + } + + /** + * Setting dialog + */ + public function settingsAction() + { + $this->createTabs(); + $this->view->dashboard = $this->dashboard; + } + + /** + * Create tab aggregation + */ + private function createTabs() + { + $this->view->tabs = $this->dashboard->getTabs()->extend(new DashboardSettings()); + } +} diff --git a/application/controllers/ErrorController.php b/application/controllers/ErrorController.php new file mode 100644 index 0000000..476b71f --- /dev/null +++ b/application/controllers/ErrorController.php @@ -0,0 +1,176 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Controllers; + +use Icinga\Application\Hook\DbMigrationHook; +use Icinga\Application\MigrationManager; +use Icinga\Exception\IcingaException; +use Zend_Controller_Plugin_ErrorHandler; +use Icinga\Application\Icinga; +use Icinga\Application\Logger; +use Icinga\Exception\Http\HttpExceptionInterface; +use Icinga\Exception\MissingParameterException; +use Icinga\Security\SecurityException; +use Icinga\Web\Controller\ActionController; +use Icinga\Web\Url; + +/** + * Application wide controller for displaying exceptions + */ +class ErrorController extends ActionController +{ + /** + * Regular expression to match exceptions resulting from missing functions/classes + */ + const MISSING_DEP_ERROR = + "/Uncaught Error:.*(?:undefined function (\S+)|Class ['\"]([^']+)['\"] not found).* in ([^:]+)/"; + + /** + * {@inheritdoc} + */ + protected $requiresAuthentication = false; + + /** + * {@inheritdoc} + */ + public function init() + { + $this->rerenderLayout = $this->params->has('renderLayout'); + } + + /** + * Display exception + */ + public function errorAction() + { + $error = $this->_getParam('error_handler'); + $exception = $error->exception; + /** @var \Exception $exception */ + + if (! ($isAuthenticated = $this->Auth()->isAuthenticated())) { + $this->innerLayout = 'guest-error'; + } + + $modules = Icinga::app()->getModuleManager(); + $sourcePath = ltrim($this->_request->get('PATH_INFO'), '/'); + $pathParts = preg_split('~/~', $sourcePath); + $moduleName = array_shift($pathParts); + + $module = null; + switch ($error->type) { + case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ROUTE: + case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_CONTROLLER: + case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ACTION: + $this->getResponse()->setHttpResponseCode(404); + $this->view->messages = array($this->translate('Page not found.')); + if ($isAuthenticated) { + if ($modules->hasInstalled($moduleName) && ! $modules->hasEnabled($moduleName)) { + $this->view->messages[0] .= ' ' . sprintf( + $this->translate('Enabling the "%s" module might help!'), + $moduleName + ); + } + } + + break; + default: + switch (true) { + case $exception instanceof HttpExceptionInterface: + $this->getResponse()->setHttpResponseCode($exception->getStatusCode()); + foreach ($exception->getHeaders() as $name => $value) { + $this->getResponse()->setHeader($name, $value, true); + } + break; + case $exception instanceof MissingParameterException: + $this->getResponse()->setHttpResponseCode(400); + $this->getResponse()->setHeader( + 'X-Status-Reason', + 'Missing parameter ' . $exception->getParameter() + ); + break; + case $exception instanceof SecurityException: + $this->getResponse()->setHttpResponseCode(403); + break; + default: + $mm = MigrationManager::instance(); + $action = $this->getRequest()->getActionName(); + $controller = $this->getRequest()->getControllerName(); + if ($action !== 'hint' && $controller !== 'migrations' && $mm->hasMigrations($moduleName)) { + // The view renderer from IPL web doesn't render the HTML content set in the respective + // controller if the error_handler request param is set, as it doesn't support error + // rendering. Since this error handler isn't caused by the migrations controller, we can + // safely unset this. + $this->setParam('error_handler', null); + $this->forward('hint', 'migrations', 'default', [ + DbMigrationHook::MIGRATION_PARAM => $moduleName + ]); + + return; + } + + $this->getResponse()->setHttpResponseCode(500); + $module = $modules->hasLoaded($moduleName) ? $modules->getModule($moduleName) : null; + Logger::error("%s\n%s", $exception, IcingaException::getConfidentialTraceAsString($exception)); + break; + } + + // Try to narrow down why the request has failed + if (preg_match(self::MISSING_DEP_ERROR, $exception->getMessage(), $match)) { + $sourcePath = $match[3]; + foreach ($modules->listLoadedModules() as $name) { + $candidate = $modules->getModule($name); + $modulePath = $candidate->getBaseDir(); + if (substr($sourcePath, 0, strlen($modulePath)) === $modulePath) { + $module = $candidate; + break; + } + } + + if (preg_match('/^(?:Icinga\\\Module\\\(\w+)|(\w+)\\\(\w+))/', $match[1] ?: $match[2], $natch)) { + $this->view->requiredModule = isset($natch[1]) ? strtolower($natch[1]) : null; + $this->view->requiredVendor = isset($natch[2]) ? $natch[2] : null; + $this->view->requiredProject = isset($natch[3]) ? $natch[3] : null; + } + } + + $this->view->messages = array(); + + if ($this->getInvokeArg('displayExceptions')) { + $this->view->stackTraces = array(); + + do { + $this->view->messages[] = $exception->getMessage(); + $this->view->stackTraces[] = IcingaException::getConfidentialTraceAsString($exception); + $exception = $exception->getPrevious(); + } while ($exception !== null); + } else { + do { + $this->view->messages[] = $exception->getMessage(); + $exception = $exception->getPrevious(); + } while ($exception !== null); + } + + break; + } + + if ($this->getRequest()->isApiRequest()) { + $this->getResponse()->json() + ->setErrorMessage($this->view->messages[0]) + ->sendResponse(); + } + + $this->view->module = $module; + $this->view->request = $error->request; + if (! $isAuthenticated) { + $this->view->hideControls = true; + } else { + $this->view->hideControls = false; + $this->getTabs()->add('error', array( + 'active' => true, + 'label' => $this->translate('Error'), + 'url' => Url::fromRequest() + )); + } + } +} diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php new file mode 100644 index 0000000..d18397c --- /dev/null +++ b/application/controllers/GroupController.php @@ -0,0 +1,418 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Controllers; + +use Exception; +use Icinga\Application\Logger; +use Icinga\Authentication\User\DomainAwareInterface; +use Icinga\Data\DataArray\ArrayDatasource; +use Icinga\Data\Filter\Filter; +use Icinga\Data\Reducible; +use Icinga\Exception\NotFoundError; +use Icinga\Forms\Config\UserGroup\AddMemberForm; +use Icinga\Forms\Config\UserGroup\UserGroupForm; +use Icinga\User; +use Icinga\Web\Controller\AuthBackendController; +use Icinga\Web\Form; +use Icinga\Web\Notification; +use Icinga\Web\Url; +use Icinga\Web\Widget; + +class GroupController extends AuthBackendController +{ + public function init() + { + $this->view->title = $this->translate('User Groups'); + + parent::init(); + } + + /** + * List all user groups of a single backend + */ + public function listAction() + { + $this->assertPermission('config/access-control/groups'); + $this->createListTabs()->activate('group/list'); + $backendNames = array_map( + function ($b) { + return $b->getName(); + }, + $this->loadUserGroupBackends('Icinga\Data\Selectable') + ); + if (empty($backendNames)) { + return; + } + + $this->view->backendSelection = new Form(); + $this->view->backendSelection->setAttrib('class', 'backend-selection icinga-controls'); + $this->view->backendSelection->setUidDisabled(); + $this->view->backendSelection->setMethod('GET'); + $this->view->backendSelection->setTokenDisabled(); + $this->view->backendSelection->addElement( + 'select', + 'backend', + array( + 'autosubmit' => true, + 'label' => $this->translate('User Group Backend'), + 'multiOptions' => array_combine($backendNames, $backendNames), + 'value' => $this->params->get('backend') + ) + ); + + $backend = $this->getUserGroupBackend($this->params->get('backend')); + if ($backend === null) { + $this->view->backend = null; + return; + } + + $query = $backend->select(array('group_name')); + + $this->view->groups = $query; + $this->view->backend = $backend; + + $this->setupPaginationControl($query); + $this->setupFilterControl($query); + $this->setupLimitControl(); + $this->setupSortControl( + array( + 'group_name' => $this->translate('User Group'), + 'created_at' => $this->translate('Created at'), + 'last_modified' => $this->translate('Last modified') + ), + $query + ); + } + + /** + * Show a group + */ + public function showAction() + { + $this->assertPermission('config/access-control/groups'); + $groupName = $this->params->getRequired('group'); + $backend = $this->getUserGroupBackend($this->params->getRequired('backend')); + + $group = $backend->select(array( + 'group_name', + 'created_at', + 'last_modified' + ))->where('group_name', $groupName)->fetchRow(); + if ($group === false) { + $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName)); + } + + $members = $backend + ->select() + ->from('group_membership', array('user_name')) + ->where('group_name', $groupName); + + $this->setupFilterControl($members, null, array('user'), array('group')); + $this->setupPaginationControl($members); + $this->setupLimitControl(); + $this->setupSortControl( + array( + 'user_name' => $this->translate('Username'), + 'created_at' => $this->translate('Created at'), + 'last_modified' => $this->translate('Last modified') + ), + $members + ); + + $this->view->group = $group; + $this->view->backend = $backend; + $this->view->members = $members; + $this->createShowTabs($backend->getName(), $groupName)->activate('group/show'); + + if ($this->hasPermission('config/access-control/groups') && $backend instanceof Reducible) { + $removeForm = new Form(); + $removeForm->setUidDisabled(); + $removeForm->setAttrib('class', 'inline'); + $removeForm->setAction( + Url::fromPath('group/removemember', array('backend' => $backend->getName(), 'group' => $groupName)) + ); + $removeForm->addElement('hidden', 'user_name', array( + 'isArray' => true, + 'decorators' => array('ViewHelper') + )); + $removeForm->addElement('hidden', 'redirect', array( + 'value' => Url::fromPath('group/show', array( + 'backend' => $backend->getName(), + 'group' => $groupName + )), + 'decorators' => array('ViewHelper') + )); + $removeForm->addElement('button', 'btn_submit', array( + 'escape' => false, + 'type' => 'submit', + 'class' => 'link-button spinner', + 'value' => 'btn_submit', + 'decorators' => array('ViewHelper'), + 'label' => $this->view->icon('trash'), + 'title' => $this->translate('Remove this member') + )); + $this->view->removeForm = $removeForm; + } + } + + /** + * Add a group + */ + public function addAction() + { + $this->assertPermission('config/access-control/groups'); + $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Extensible'); + $form = new UserGroupForm(); + $form->setRedirectUrl(Url::fromPath('group/list', array('backend' => $backend->getName()))); + $form->setRepository($backend); + $form->add()->handleRequest(); + + $this->renderForm($form, $this->translate('New User Group')); + } + + /** + * Edit a group + */ + public function editAction() + { + $this->assertPermission('config/access-control/groups'); + $groupName = $this->params->getRequired('group'); + $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Updatable'); + + $form = new UserGroupForm(); + $form->setRedirectUrl( + Url::fromPath('group/show', array('backend' => $backend->getName(), 'group' => $groupName)) + ); + $form->setRepository($backend); + + try { + $form->edit($groupName)->handleRequest(); + } catch (NotFoundError $_) { + $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName)); + } + + $this->renderForm($form, $this->translate('Update User Group')); + } + + /** + * Remove a group + */ + public function removeAction() + { + $this->assertPermission('config/access-control/groups'); + $groupName = $this->params->getRequired('group'); + $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Reducible'); + + $form = new UserGroupForm(); + $form->setRedirectUrl(Url::fromPath('group/list', array('backend' => $backend->getName()))); + $form->setRepository($backend); + + try { + $form->remove($groupName)->handleRequest(); + } catch (NotFoundError $_) { + $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName)); + } + + $this->renderForm($form, $this->translate('Remove User Group')); + } + + /** + * Add a group member + */ + public function addmemberAction() + { + $this->assertPermission('config/access-control/groups'); + $groupName = $this->params->getRequired('group'); + $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Extensible'); + + $form = new AddMemberForm(); + $form->setDataSource($this->fetchUsers()) + ->setBackend($backend) + ->setGroupName($groupName) + ->setRedirectUrl( + Url::fromPath('group/show', array('backend' => $backend->getName(), 'group' => $groupName)) + ) + ->setUidDisabled(); + + try { + $form->handleRequest(); + } catch (NotFoundError $_) { + $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName)); + } + + $this->renderForm($form, $this->translate('New User Group Member')); + } + + /** + * Remove a group member + */ + public function removememberAction() + { + $this->assertPermission('config/access-control/groups'); + $this->assertHttpMethod('POST'); + $groupName = $this->params->getRequired('group'); + $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Reducible'); + + $form = new Form(array( + 'onSuccess' => function ($form) use ($groupName, $backend) { + foreach ($form->getValue('user_name') as $userName) { + try { + $backend->delete( + 'group_membership', + Filter::matchAll( + Filter::where('group_name', $groupName), + Filter::where('user_name', $userName) + ) + ); + Notification::success(sprintf( + t('User "%s" has been removed from group "%s"'), + $userName, + $groupName + )); + } catch (NotFoundError $e) { + throw $e; + } catch (Exception $e) { + Notification::error($e->getMessage()); + } + } + + $redirect = $form->getValue('redirect'); + if (! empty($redirect)) { + $form->setRedirectUrl(htmlspecialchars_decode($redirect)); + } + + return true; + } + )); + $form->setUidDisabled(); + $form->setSubmitLabel('btn_submit'); // Required to ensure that isSubmitted() is called + $form->addElement('hidden', 'user_name', array('required' => true, 'isArray' => true)); + $form->addElement('hidden', 'redirect'); + + try { + $form->handleRequest(); + } catch (NotFoundError $_) { + $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName)); + } + } + + /** + * Fetch and return all users from all user backends + * + * @return ArrayDatasource + */ + protected function fetchUsers() + { + $users = array(); + foreach ($this->loadUserBackends('Icinga\Data\Selectable') as $backend) { + try { + if ($backend instanceof DomainAwareInterface) { + $domain = $backend->getDomain(); + } else { + $domain = null; + } + foreach ($backend->select(array('user_name')) as $user) { + $userObj = new User($user->user_name); + if ($domain !== null) { + if ($userObj->hasDomain() && $userObj->getDomain() !== $domain) { + // Users listed in a user backend which is configured to be responsible for a domain should + // not have a domain in their username. Ultimately, if the username has a domain, it must + // not differ from the backend's domain. We could log here - but hey, who cares :) + continue; + } else { + $userObj->setDomain($domain); + } + } + + $user->user_name = $userObj->getUsername(); + + $users[] = $user; + } + } catch (Exception $e) { + Logger::error($e); + Notification::warning(sprintf( + $this->translate('Failed to fetch any users from backend %s. Please check your log'), + $backend->getName() + )); + } + } + + return new ArrayDatasource($users); + } + + /** + * Create the tabs to display when showing a group + * + * @param string $backendName + * @param string $groupName + */ + protected function createShowTabs($backendName, $groupName) + { + $tabs = $this->getTabs(); + $tabs->add( + 'group/show', + array( + 'title' => sprintf($this->translate('Show group %s'), $groupName), + 'label' => $this->translate('Group'), + 'url' => Url::fromPath('group/show', array('backend' => $backendName, 'group' => $groupName)) + ) + ); + + return $tabs; + } + + /** + * Create the tabs to display when listing groups + */ + protected function createListTabs() + { + $tabs = $this->getTabs(); + + if ($this->hasPermission('config/access-control/roles')) { + $tabs->add( + 'role/list', + array( + 'baseTarget' => '_main', + 'label' => $this->translate('Roles'), + 'title' => $this->translate( + 'Configure roles to permit or restrict users and groups accessing Icinga Web 2' + ), + 'url' => 'role/list' + ) + ); + + $tabs->add( + 'role/audit', + [ + 'title' => $this->translate('Audit a user\'s or group\'s privileges'), + 'label' => $this->translate('Audit'), + 'url' => 'role/audit', + 'baseTarget' => '_main', + ] + ); + } + + if ($this->hasPermission('config/access-control/users')) { + $tabs->add( + 'user/list', + array( + 'title' => $this->translate('List users of authentication backends'), + 'label' => $this->translate('Users'), + 'url' => 'user/list' + ) + ); + } + + $tabs->add( + 'group/list', + array( + 'title' => $this->translate('List groups of user group backends'), + 'label' => $this->translate('User Groups'), + 'url' => 'group/list' + ) + ); + + return $tabs; + } +} diff --git a/application/controllers/HealthController.php b/application/controllers/HealthController.php new file mode 100644 index 0000000..f176e69 --- /dev/null +++ b/application/controllers/HealthController.php @@ -0,0 +1,65 @@ +<?php +/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */ + +namespace Icinga\Controllers; + +use Icinga\Application\Hook\HealthHook; +use Icinga\Web\View\AppHealth; +use Icinga\Web\Widget\Tabextension\OutputFormat; +use ipl\Html\Html; +use ipl\Html\HtmlString; +use ipl\Web\Compat\CompatController; + +class HealthController extends CompatController +{ + public function indexAction() + { + $query = HealthHook::collectHealthData() + ->select(); + + $this->setupSortControl( + [ + 'module' => $this->translate('Module'), + 'name' => $this->translate('Name'), + 'state' => $this->translate('State') + ], + $query, + ['state' => 'desc'] + ); + $this->setupLimitControl(); + $this->setupPaginationControl($query); + $this->setupFilterControl($query, [ + 'module' => $this->translate('Module'), + 'name' => $this->translate('Name'), + 'state' => $this->translate('State'), + 'message' => $this->translate('Message') + ], ['name'], ['format']); + + $this->getTabs()->extend(new OutputFormat(['csv'])); + $this->handleFormatRequest($query); + + $this->addControl(HtmlString::create((string) $this->view->paginator)); + $this->addControl(Html::tag('div', ['class' => 'sort-controls-container'], [ + HtmlString::create((string) $this->view->limiter), + HtmlString::create((string) $this->view->sortBox) + ])); + $this->addControl(HtmlString::create((string) $this->view->filterEditor)); + + $this->addTitleTab(t('Health')); + $this->setAutorefreshInterval(10); + $this->addContent(new AppHealth($query)); + } + + protected function handleFormatRequest($query) + { + $formatJson = $this->params->get('format') === 'json'; + if (! $formatJson && ! $this->getRequest()->isApiRequest()) { + return; + } + + $this->getResponse() + ->json() + ->setSuccessData($query->fetchAll()) + ->sendResponse(); + } +} diff --git a/application/controllers/IframeController.php b/application/controllers/IframeController.php new file mode 100644 index 0000000..8aebba4 --- /dev/null +++ b/application/controllers/IframeController.php @@ -0,0 +1,20 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Controllers; + +use Icinga\Web\Controller; + +/** + * Display external or internal links within an iframe + */ +class IframeController extends Controller +{ + /** + * Display iframe w/ the given URL + */ + public function indexAction() + { + $this->view->url = $this->params->getRequired('url'); + } +} diff --git a/application/controllers/IndexController.php b/application/controllers/IndexController.php new file mode 100644 index 0000000..539c16b --- /dev/null +++ b/application/controllers/IndexController.php @@ -0,0 +1,36 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Controllers; + +use Icinga\Web\Controller\ActionController; +use Icinga\Web\Url; + +/** + * Application wide index controller + */ +class IndexController extends ActionController +{ + /** + * Use a default redirection rule to welcome page + */ + public function preDispatch() + { + if ($this->getRequest()->getActionName() !== 'welcome') { + $landingPage = getenv('ICINGAWEB_LANDING_PAGE'); + if (! $landingPage) { + $landingPage = 'dashboard'; + } + + // @TODO(el): Avoid landing page redirects: https://dev.icinga.com/issues/9656 + $this->redirectNow(Url::fromRequest()->setPath($landingPage)); + } + } + + /** + * Application's start page + */ + public function welcomeAction() + { + } +} diff --git a/application/controllers/LayoutController.php b/application/controllers/LayoutController.php new file mode 100644 index 0000000..237681c --- /dev/null +++ b/application/controllers/LayoutController.php @@ -0,0 +1,28 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Controllers; + +use Icinga\Web\Controller\ActionController; +use Icinga\Web\Menu; + +/** + * Create complex layout parts + */ +class LayoutController extends ActionController +{ + /** + * Render the menu + */ + public function menuAction() + { + $this->setAutorefreshInterval(15); + $this->_helper->layout()->disableLayout(); + $this->view->menuRenderer = (new Menu())->getRenderer(); + } + + public function announcementsAction() + { + $this->_helper->layout()->disableLayout(); + } +} diff --git a/application/controllers/ListController.php b/application/controllers/ListController.php new file mode 100644 index 0000000..2fbc5a9 --- /dev/null +++ b/application/controllers/ListController.php @@ -0,0 +1,59 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Controllers; + +use Icinga\Application\Config; +use Icinga\Application\Logger; +use Icinga\Data\ConfigObject; +use Icinga\Protocol\File\FileReader; +use Icinga\Web\Controller; +use Icinga\Web\Url; +use Icinga\Web\Widget\Tabextension\DashboardAction; +use Icinga\Web\Widget\Tabextension\MenuAction; +use Icinga\Web\Widget\Tabextension\OutputFormat; + +/** + * Application wide controller for various listing actions + */ +class ListController extends Controller +{ + /** + * Add title tab + * + * @param string $action + */ + protected function addTitleTab($action) + { + $this->getTabs()->add($action, array( + 'label' => ucfirst($action), + 'url' => Url::fromPath('list/' . str_replace(' ', '', $action)) + ))->extend(new OutputFormat())->extend(new DashboardAction())->extend(new MenuAction())->activate($action); + } + + /** + * Display the application log + */ + public function applicationlogAction() + { + $this->assertPermission('application/log'); + + if (! Logger::writesToFile()) { + $this->httpNotFound('Page not found'); + } + + $this->addTitleTab('application log'); + + $resource = new FileReader(new ConfigObject(array( + 'filename' => Config::app()->get('logging', 'file'), + 'fields' => '/(?<!.)(?<datetime>[0-9]{4}(?:-[0-9]{2}){2}' // date + . 'T[0-9]{2}(?::[0-9]{2}){2}(?:[\+\-][0-9]{2}:[0-9]{2})?)' // time + . ' - (?<loglevel>[A-Za-z]+) - (?<message>.*)(?!.)/msS' // loglevel, message + ))); + $this->view->logData = $resource->select()->order('DESC'); + + $this->setupLimitControl(); + $this->setupPaginationControl($this->view->logData); + $this->view->title = $this->translate('Application Log'); + } +} diff --git a/application/controllers/ManageUserDevicesController.php b/application/controllers/ManageUserDevicesController.php new file mode 100644 index 0000000..db054d1 --- /dev/null +++ b/application/controllers/ManageUserDevicesController.php @@ -0,0 +1,84 @@ +<?php +/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */ + +namespace Icinga\Controllers; + +use Icinga\Common\Database; +use Icinga\Web\Notification; +use Icinga\Web\RememberMe; +use Icinga\Web\RememberMeUserList; +use Icinga\Web\RememberMeUserDevicesList; +use ipl\Web\Compat\CompatController; +use ipl\Web\Url; + +/** + * ManageUserDevicesController + * + * you need 'application/sessions' permission to use this controller + */ +class ManageUserDevicesController extends CompatController +{ + use Database; + + public function init() + { + $this->assertPermission('application/sessions'); + } + + public function indexAction() + { + $this->getTabs() + ->add( + 'manage-user-devices', + array( + 'title' => $this->translate('List of users who stay logged in'), + 'label' => $this->translate('Users'), + 'url' => 'manage-user-devices', + 'data-base-target' => '_self' + ) + )->activate('manage-user-devices'); + + $usersList = (new RememberMeUserList()) + ->setUsers(RememberMe::getAllUser()) + ->setUrl('manage-user-devices/devices'); + + $this->addContent($usersList); + + if (! $this->hasDb()) { + Notification::warning( + $this->translate("Users can't stay logged in without a database configuration backend") + ); + } + } + + public function devicesAction() + { + $this->getTabs() + ->add( + 'manage-devices', + array( + 'title' => $this->translate('List of devices'), + 'label' => $this->translate('Devices'), + 'url' => 'manage-user-devices/devices' + ) + )->activate('manage-devices'); + + $name = $this->params->getRequired('name'); + $data = (new RememberMeUserDevicesList()) + ->setDevicesList(RememberMe::getAllByUsername($name)) + ->setUsername($name) + ->setUrl('manage-user-devices/delete'); + + $this->addContent($data); + } + + public function deleteAction() + { + (new RememberMe())->remove($this->params->getRequired('fingerprint')); + + $this->redirectNow( + Url::fromPath('manage-user-devices/devices') + ->addParams(['name' => $this->params->getRequired('name')]) + ); + } +} diff --git a/application/controllers/MigrationsController.php b/application/controllers/MigrationsController.php new file mode 100644 index 0000000..5229f06 --- /dev/null +++ b/application/controllers/MigrationsController.php @@ -0,0 +1,249 @@ +<?php + +/* Icinga Web 2 | (c) 2023 Icinga GmbH | GPLv2+ */ + +namespace Icinga\Controllers; + +use Icinga\Application\Hook\DbMigrationHook; +use Icinga\Application\Icinga; +use Icinga\Application\MigrationManager; +use Icinga\Common\Database; +use Icinga\Exception\MissingParameterException; +use Icinga\Forms\MigrationForm; +use Icinga\Web\Notification; +use Icinga\Web\Widget\ItemList\MigrationList; +use Icinga\Web\Widget\Tabextension\OutputFormat; +use ipl\Html\Attributes; +use ipl\Html\FormElement\SubmitButtonElement; +use ipl\Html\HtmlElement; +use ipl\Html\Text; +use ipl\Web\Compat\CompatController; +use ipl\Web\Widget\ActionLink; + +class MigrationsController extends CompatController +{ + use Database; + + public function init() + { + Icinga::app()->getModuleManager()->loadModule('setup'); + } + + public function indexAction(): void + { + $mm = MigrationManager::instance(); + + $this->getTabs()->extend(new OutputFormat(['csv'])); + $this->addTitleTab($this->translate('Migrations')); + + $canApply = $this->hasPermission('application/migrations'); + if (! $canApply) { + $this->addControl( + new HtmlElement( + 'div', + Attributes::create(['class' => 'migration-state-banner']), + new HtmlElement( + 'span', + null, + Text::create( + $this->translate('You do not have the required permission to apply pending migrations.') + ) + ) + ) + ); + } + + $migrateListForm = new MigrationForm(); + $migrateListForm->setAttribute('id', $this->getRequest()->protectId('migration-form')); + $migrateListForm->setRenderDatabaseUserChange(! $mm->validateDatabasePrivileges()); + + if ($canApply && $mm->hasPendingMigrations()) { + $migrateAllButton = new SubmitButtonElement(sprintf('migrate-%s', DbMigrationHook::ALL_MIGRATIONS), [ + 'form' => $migrateListForm->getAttribute('id')->getValue(), + 'label' => $this->translate('Migrate All'), + 'title' => $this->translate('Migrate all pending migrations') + ]); + + // Is the first button, so will be cloned and that the visible + // button is outside the form doesn't matter for Web's JS + $migrateListForm->registerElement($migrateAllButton); + + // Make sure it looks familiar, even if not inside a form + $migrateAllButton->setWrapper(new HtmlElement('div', Attributes::create(['class' => 'icinga-controls']))); + + $this->controls->getAttributes()->add('class', 'default-layout'); + $this->addControl($migrateAllButton); + } + + $this->handleFormatRequest($mm->toArray()); + + $frameworkList = new MigrationList($mm->yieldMigrations(), $migrateListForm); + $frameworkListControl = new HtmlElement('div', Attributes::create(['class' => 'migration-list-control'])); + $frameworkListControl->addHtml(new HtmlElement('h2', null, Text::create($this->translate('System')))); + $frameworkListControl->addHtml($frameworkList); + + $moduleList = new MigrationList($mm->yieldMigrations(true), $migrateListForm); + $moduleListControl = new HtmlElement('div', Attributes::create(['class' => 'migration-list-control'])); + $moduleListControl->addHtml(new HtmlElement('h2', null, Text::create($this->translate('Modules')))); + $moduleListControl->addHtml($moduleList); + + $migrateListForm->addHtml($frameworkListControl, $moduleListControl); + if ($canApply && $mm->hasPendingMigrations()) { + $frameworkList->ensureAssembled(); + $moduleList->ensureAssembled(); + + $this->handleMigrateRequest($migrateListForm); + } + + $migrations = new HtmlElement('div', Attributes::create(['class' => 'migrations'])); + $migrations->addHtml($migrateListForm); + + $this->addContent($migrations); + } + + public function hintAction(): void + { + // The forwarded request doesn't modify the original server query string, but adds the migration param to the + // request param instead. So, there is no way to access the migration param other than via the request instance. + /** @var ?string $module */ + $module = $this->getRequest()->getParam(DbMigrationHook::MIGRATION_PARAM); + if ($module === null) { + throw new MissingParameterException( + $this->translate('Required parameter \'%s\' missing'), + DbMigrationHook::MIGRATION_PARAM + ); + } + + $mm = MigrationManager::instance(); + if (! $mm->hasMigrations($module)) { + $this->httpNotFound(sprintf('There are no pending migrations matching the given name: %s', $module)); + } + + $migration = $mm->getMigration($module); + $this->addTitleTab($this->translate('Error')); + $this->addContent( + new HtmlElement( + 'div', + Attributes::create(['class' => 'pending-migrations-hint']), + new HtmlElement('h2', null, Text::create($this->translate('Error!'))), + new HtmlElement( + 'p', + null, + Text::create(sprintf($this->translate('%s has pending migrations.'), $migration->getName())) + ), + new HtmlElement('p', null, Text::create($this->translate('Please apply the migrations first.'))), + new ActionLink($this->translate('View pending Migrations'), 'migrations') + ) + ); + } + + public function migrationAction(): void + { + /** @var string $name */ + $name = $this->params->getRequired(DbMigrationHook::MIGRATION_PARAM); + + $this->addTitleTab($this->translate('Migration')); + $this->getTabs()->disableLegacyExtensions(); + $this->controls->getAttributes()->add('class', 'default-layout'); + + $mm = MigrationManager::instance(); + if (! $mm->hasMigrations($name)) { + $migrations = []; + } else { + $hook = $mm->getMigration($name); + $migrations = array_reverse($hook->getMigrations()); + if (! $this->hasPermission('application/migrations')) { + $this->addControl( + new HtmlElement( + 'div', + Attributes::create(['class' => 'migration-state-banner']), + new HtmlElement( + 'span', + null, + Text::create( + $this->translate('You do not have the required permission to apply pending migrations.') + ) + ) + ) + ); + } else { + $this->addControl( + new HtmlElement( + 'div', + Attributes::create(['class' => 'migration-controls']), + new HtmlElement('span', null, Text::create($hook->getName())) + ) + ); + } + } + + $migrationWidget = new HtmlElement('div', Attributes::create(['class' => 'migrations'])); + $migrationWidget->addHtml((new MigrationList($migrations))->setMinimal(false)); + $this->addContent($migrationWidget); + } + + public function handleMigrateRequest(MigrationForm $form): void + { + $this->assertPermission('application/migrations'); + + $form->on(MigrationForm::ON_SUCCESS, function (MigrationForm $form) { + $mm = MigrationManager::instance(); + + /** @var array<string, string> $elevatedPrivileges */ + $elevatedPrivileges = $form->getValue('database_setup'); + if ($elevatedPrivileges !== null && $elevatedPrivileges['grant_privileges'] === 'y') { + $mm->fixIcingaWebMysqlGrants($this->getDb(), $elevatedPrivileges); + } + + $pressedButton = $form->getPressedSubmitElement(); + if ($pressedButton) { + $name = substr($pressedButton->getName(), 8); + switch ($name) { + case DbMigrationHook::ALL_MIGRATIONS: + if ($mm->applyAll($elevatedPrivileges)) { + Notification::success($this->translate('Applied all migrations successfully')); + } else { + Notification::error( + $this->translate( + 'Applied migrations successfully. Though, one or more migration hooks' + . ' failed to run. See logs for details' + ) + ); + } + break; + default: + $migration = $mm->getMigration($name); + if ($mm->apply($migration, $elevatedPrivileges)) { + Notification::success($this->translate('Applied pending migrations successfully')); + } else { + Notification::error( + $this->translate('Failed to apply pending migration(s). See logs for details') + ); + } + } + } + + $this->sendExtraUpdates(['#col2' => '__CLOSE__']); + + $this->redirectNow('migrations'); + })->handleRequest($this->getServerRequest()); + } + + /** + * Handle exports + * + * @param array<string, mixed> $data + */ + protected function handleFormatRequest(array $data): void + { + $formatJson = $this->params->get('format') === 'json'; + if (! $formatJson && ! $this->getRequest()->isApiRequest()) { + return; + } + + $this->getResponse() + ->json() + ->setSuccessData($data) + ->sendResponse(); + } +} diff --git a/application/controllers/MyDevicesController.php b/application/controllers/MyDevicesController.php new file mode 100644 index 0000000..e0fb98a --- /dev/null +++ b/application/controllers/MyDevicesController.php @@ -0,0 +1,74 @@ +<?php +/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */ + +namespace Icinga\Controllers; + +use Icinga\Common\Database; +use Icinga\Web\Notification; +use Icinga\Web\RememberMe; +use Icinga\Web\RememberMeUserDevicesList; +use ipl\Web\Compat\CompatController; + +/** + * MyDevicesController + * + * this controller shows you all the devices you are logged in + */ +class MyDevicesController extends CompatController +{ + use Database; + + public function init() + { + $this->getTabs() + ->add( + 'account', + array( + 'title' => $this->translate('Update your account'), + 'label' => $this->translate('My Account'), + 'url' => 'account' + ) + ) + ->add( + 'navigation', + array( + 'title' => $this->translate('List and configure your own navigation items'), + 'label' => $this->translate('Navigation'), + 'url' => 'navigation' + ) + ) + ->add( + 'devices', + array( + 'title' => $this->translate('List of devices you are logged in'), + 'label' => $this->translate('My Devices'), + 'url' => 'my-devices' + ) + )->activate('devices'); + } + + public function indexAction() + { + $name = $this->auth->getUser()->getUsername(); + + $data = (new RememberMeUserDevicesList()) + ->setDevicesList(RememberMe::getAllByUsername($name)) + ->setUsername($name) + ->setUrl('my-devices/delete'); + + $this->addContent($data); + + if (! $this->hasDb()) { + Notification::warning( + $this->translate("Users can't stay logged in without a database configuration backend") + ); + } + } + + public function deleteAction() + { + (new RememberMe())->remove($this->params->getRequired('fingerprint')); + + $this->redirectNow('my-devices'); + } +} diff --git a/application/controllers/NavigationController.php b/application/controllers/NavigationController.php new file mode 100644 index 0000000..b0babc3 --- /dev/null +++ b/application/controllers/NavigationController.php @@ -0,0 +1,447 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Controllers; + +use Exception; +use Icinga\Application\Config; +use Icinga\Exception\NotFoundError; +use Icinga\Data\DataArray\ArrayDatasource; +use Icinga\Data\Filter\FilterMatchCaseInsensitive; +use Icinga\Forms\ConfirmRemovalForm; +use Icinga\Forms\Navigation\NavigationConfigForm; +use Icinga\Web\Controller; +use Icinga\Web\Form; +use Icinga\Web\Menu; +use Icinga\Web\Navigation\Navigation; +use Icinga\Web\Notification; +use Icinga\Web\Url; + +/** + * Navigation configuration + */ +class NavigationController extends Controller +{ + /** + * The global navigation item type configuration + * + * @var array + */ + protected $itemTypeConfig; + + /** + * {@inheritdoc} + */ + public function init() + { + parent::init(); + $this->itemTypeConfig = Navigation::getItemTypeConfiguration(); + } + + /** + * Return the label for the given navigation item type + * + * @param string $type + * + * @return string $type if no label can be found + */ + protected function getItemLabel($type) + { + return isset($this->itemTypeConfig[$type]['label']) ? $this->itemTypeConfig[$type]['label'] : $type; + } + + /** + * Return a list of available navigation item types + * + * @return array + */ + protected function listItemTypes() + { + $types = array(); + foreach ($this->itemTypeConfig as $type => $options) { + $types[$type] = isset($options['label']) ? $options['label'] : $type; + } + + return $types; + } + + /** + * Return all shared navigation item configurations + * + * @param string $owner A username if only items shared by a specific user are desired + * + * @return array + */ + protected function fetchSharedNavigationItemConfigs($owner = null) + { + $configs = array(); + foreach ($this->itemTypeConfig as $type => $_) { + $config = Config::navigation($type); + $config->getConfigObject()->setKeyColumn('name'); + $query = $config->select(); + if ($owner !== null) { + $query->applyFilter(new FilterMatchCaseInsensitive('owner', '=', $owner)); + } + + foreach ($query as $itemConfig) { + $configs[] = $itemConfig; + } + } + + return $configs; + } + + /** + * Return all user navigation item configurations + * + * @param string $username + * + * @return array + */ + protected function fetchUserNavigationItemConfigs($username) + { + $configs = array(); + foreach ($this->itemTypeConfig as $type => $_) { + $config = Config::navigation($type, $username); + $config->getConfigObject()->setKeyColumn('name'); + foreach ($config->select() as $itemConfig) { + $configs[] = $itemConfig; + } + } + + return $configs; + } + + /** + * Show the current user a list of his/her navigation items + */ + public function indexAction() + { + $user = $this->Auth()->getUser(); + $ds = new ArrayDatasource(array_merge( + $this->fetchSharedNavigationItemConfigs($user->getUsername()), + $this->fetchUserNavigationItemConfigs($user->getUsername()) + )); + $query = $ds->select(); + + $this->view->types = $this->listItemTypes(); + $this->view->items = $query; + + $this->view->title = $this->translate('Navigation'); + $this->getTabs() + ->add( + 'account', + array( + 'title' => $this->translate('Update your account'), + 'label' => $this->translate('My Account'), + 'url' => 'account' + ) + ) + ->add( + 'navigation', + array( + 'active' => true, + 'title' => $this->translate('List and configure your own navigation items'), + 'label' => $this->translate('Navigation'), + 'url' => 'navigation' + ) + ) + ->add( + 'devices', + array( + 'title' => $this->translate('List of devices you are logged in'), + 'label' => $this->translate('My Devices'), + 'url' => 'my-devices' + ) + ); + $this->setupSortControl( + array( + 'type' => $this->translate('Type'), + 'owner' => $this->translate('Shared'), + 'name' => $this->translate('Navigation') + ), + $query + ); + } + + /** + * List all shared navigation items + */ + public function sharedAction() + { + $this->assertPermission('config/navigation'); + $ds = new ArrayDatasource($this->fetchSharedNavigationItemConfigs()); + $query = $ds->select(); + + $removeForm = new Form(); + $removeForm->setUidDisabled(); + $removeForm->setAttrib('class', 'inline'); + $removeForm->addElement('hidden', 'name', array( + 'decorators' => array('ViewHelper') + )); + $removeForm->addElement('hidden', 'redirect', array( + 'value' => Url::fromPath('navigation/shared'), + 'decorators' => array('ViewHelper') + )); + $removeForm->addElement('button', 'btn_submit', array( + 'escape' => false, + 'type' => 'submit', + 'class' => 'link-button spinner', + 'value' => 'btn_submit', + 'decorators' => array('ViewHelper'), + 'label' => $this->view->icon('trash'), + 'title' => $this->translate('Unshare this navigation item') + )); + + $this->view->removeForm = $removeForm; + $this->view->types = $this->listItemTypes(); + $this->view->items = $query; + + $this->view->title = $this->translate('Shared Navigation'); + $this->getTabs()->add( + 'navigation/shared', + array( + 'title' => $this->translate('List and configure shared navigation items'), + 'label' => $this->translate('Shared Navigation'), + 'url' => 'navigation/shared' + ) + )->activate('navigation/shared'); + $this->setupSortControl( + array( + 'type' => $this->translate('Type'), + 'owner' => $this->translate('Owner'), + 'name' => $this->translate('Shared Navigation') + ), + $query + ); + } + + /** + * Add a navigation item + */ + public function addAction() + { + $form = new NavigationConfigForm(); + $form->setRedirectUrl('navigation'); + $form->setUser($this->Auth()->getUser()); + $form->setItemTypes($this->listItemTypes()); + $form->addDescription($this->translate('Create a new navigation item, such as a menu entry or dashlet.')); + + // TODO: Fetch all "safe" parameters from the url and populate them + $form->setDefaultUrl(rawurldecode($this->params->get('url', ''))); + + $form->setOnSuccess(function (NavigationConfigForm $form) { + $data = $form::transformEmptyValuesToNull($form->getValues()); + + try { + $form->add($data); + } catch (Exception $e) { + $form->error($e->getMessage()); + return false; + } + + if ($form->save()) { + if ($data['type'] === 'menu-item') { + $form->getResponse()->setRerenderLayout(); + } + + Notification::success(t('Navigation item successfully created')); + return true; + } + + return false; + }); + $form->handleRequest(); + + $this->view->title = $this->translate('Navigation'); + $this->renderForm($form, $this->translate('New Navigation Item')); + } + + /** + * Edit a navigation item + */ + public function editAction() + { + $itemName = $this->params->getRequired('name'); + $itemType = $this->params->getRequired('type'); + $referrer = $this->params->get('referrer', 'index'); + + $user = $this->Auth()->getUser(); + if ($user->can('config/navigation')) { + $itemOwner = $this->params->get('owner', $user->getUsername()); + } else { + $itemOwner = $user->getUsername(); + } + + $form = new NavigationConfigForm(); + $form->setUser($user); + $form->setShareConfig(Config::navigation($itemType)); + $form->setUserConfig(Config::navigation($itemType, $itemOwner)); + $form->setRedirectUrl($referrer === 'shared' ? 'navigation/shared' : 'navigation'); + $form->setOnSuccess(function (NavigationConfigForm $form) use ($itemName) { + $data = $form::transformEmptyValuesToNull($form->getValues()); + + try { + $form->edit($itemName, $data); + } catch (NotFoundError $e) { + throw $e; + } catch (Exception $e) { + $form->error($e->getMessage()); + return false; + } + + if ($form->save()) { + if (isset($data['type']) && $data['type'] === 'menu-item') { + $form->getResponse()->setRerenderLayout(); + } + + Notification::success(sprintf(t('Navigation item "%s" successfully updated'), $itemName)); + return true; + } + + return false; + }); + + try { + $form->load($itemName); + $form->handleRequest(); + } catch (NotFoundError $_) { + $this->httpNotFound(sprintf($this->translate('Navigation item "%s" not found'), $itemName)); + } + + $this->view->title = $this->translate('Navigation'); + $this->renderForm($form, $this->translate('Update Navigation Item')); + } + + /** + * Remove a navigation item + */ + public function removeAction() + { + $itemName = $this->params->getRequired('name'); + $itemType = $this->params->getRequired('type'); + $user = $this->Auth()->getUser(); + + $navigationConfigForm = new NavigationConfigForm(); + $navigationConfigForm->setUser($user); + $navigationConfigForm->setShareConfig(Config::navigation($itemType)); + $navigationConfigForm->setUserConfig(Config::navigation($itemType, $user->getUsername())); + + $form = new ConfirmRemovalForm(); + $form->setRedirectUrl('navigation'); + $form->setOnSuccess(function (ConfirmRemovalForm $form) use ($itemName, $navigationConfigForm) { + try { + $itemConfig = $navigationConfigForm->delete($itemName); + } catch (NotFoundError $e) { + Notification::success(sprintf(t('Navigation Item "%s" not found. No action required'), $itemName)); + return true; + } catch (Exception $e) { + $form->error($e->getMessage()); + return false; + } + + if ($navigationConfigForm->save()) { + if ($itemConfig->type === 'menu-item') { + $form->getResponse()->setRerenderLayout(); + } + + Notification::success(sprintf(t('Navigation Item "%s" successfully removed'), $itemName)); + return true; + } + + return false; + }); + $form->handleRequest(); + + $this->view->title = $this->translate('Navigation'); + $this->renderForm($form, $this->translate('Remove Navigation Item')); + } + + /** + * Unshare a navigation item + */ + public function unshareAction() + { + $this->assertPermission('config/navigation'); + $this->assertHttpMethod('POST'); + + // TODO: I'd like these being form fields + $itemType = $this->params->getRequired('type'); + $itemOwner = $this->params->getRequired('owner'); + + $navigationConfigForm = new NavigationConfigForm(); + $navigationConfigForm->setUser($this->Auth()->getUser()); + $navigationConfigForm->setShareConfig(Config::navigation($itemType)); + $navigationConfigForm->setUserConfig(Config::navigation($itemType, $itemOwner)); + + $form = new Form(array( + 'onSuccess' => function ($form) use ($navigationConfigForm) { + $itemName = $form->getValue('name'); + + try { + $newConfig = $navigationConfigForm->unshare($itemName); + if ($navigationConfigForm->save()) { + if ($newConfig->getSection($itemName)->type === 'menu-item') { + $form->getResponse()->setRerenderLayout(); + } + + Notification::success(sprintf( + t('Navigation item "%s" has been unshared'), + $form->getValue('name') + )); + } else { + // TODO: It failed obviously to write one of the configs, so we're leaving the user in + // a inconsistent state. Luckily, it's nothing lost but possibly duplicated... + Notification::error(sprintf( + t('Failed to unshare navigation item "%s"'), + $form->getValue('name') + )); + } + } catch (NotFoundError $e) { + throw $e; + } catch (Exception $e) { + Notification::error($e->getMessage()); + } + + $redirect = $form->getValue('redirect'); + if (! empty($redirect)) { + $form->setRedirectUrl(htmlspecialchars_decode($redirect)); + } + + return true; + } + )); + $form->setUidDisabled(); + $form->setSubmitLabel('btn_submit'); // Required to ensure that isSubmitted() is called + $form->addElement('hidden', 'name', array('required' => true)); + $form->addElement('hidden', 'redirect'); + + try { + $form->handleRequest(); + } catch (NotFoundError $_) { + $this->httpNotFound(sprintf($this->translate('Navigation item "%s" not found'), $form->getValue('name'))); + } + } + + public function dashboardAction() + { + $name = $this->params->getRequired('name'); + + $this->getTabs()->add('dashboard', array( + 'active' => true, + 'label' => ucwords($name), + 'url' => Url::fromRequest() + )); + + $menu = new Menu(); + + $navigation = $menu->findItem($name); + + if ($navigation === null) { + $this->httpNotFound($this->translate('Navigation not found')); + } + + $this->view->navigation = $navigation; + $this->view->title = $navigation->getLabel(); + } +} diff --git a/application/controllers/RoleController.php b/application/controllers/RoleController.php new file mode 100644 index 0000000..4223d33 --- /dev/null +++ b/application/controllers/RoleController.php @@ -0,0 +1,392 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Controllers; + +use Exception; +use GuzzleHttp\Psr7\ServerRequest; +use Icinga\Authentication\AdmissionLoader; +use Icinga\Authentication\Auth; +use Icinga\Authentication\RolesConfig; +use Icinga\Authentication\User\DomainAwareInterface; +use Icinga\Data\Selectable; +use Icinga\Exception\NotFoundError; +use Icinga\Forms\Security\RoleForm; +use Icinga\Repository\Repository; +use Icinga\Security\SecurityException; +use Icinga\User; +use Icinga\Web\Controller\AuthBackendController; +use Icinga\Web\View\PrivilegeAudit; +use Icinga\Web\Widget\SingleValueSearchControl; +use ipl\Html\Html; +use ipl\Html\HtmlString; +use ipl\Web\Url; +use ipl\Web\Widget\Link; + +/** + * Manage user permissions and restrictions based on roles + * + * @TODO(el): Rename to RolesController: https://dev.icinga.com/issues/10015 + */ +class RoleController extends AuthBackendController +{ + public function init() + { + $this->assertPermission('config/access-control/roles'); + $this->view->title = $this->translate('Roles'); + + parent::init(); + } + + public function indexAction() + { + if ($this->hasPermission('config/access-control/roles')) { + $this->redirectNow('role/list'); + } elseif ($this->hasPermission('config/access-control/users')) { + $this->redirectNow('user/list'); + } elseif ($this->hasPermission('config/access-control/groups')) { + $this->redirectNow('group/list'); + } else { + throw new SecurityException('No permission to configure Icinga Web 2'); + } + } + + /** + * List roles + * + * @TODO(el): Rename to indexAction() + */ + public function listAction() + { + $this->createListTabs()->activate('role/list'); + $this->view->roles = (new RolesConfig()) + ->select(); + + $sortAndFilterColumns = [ + 'name' => $this->translate('Name'), + 'users' => $this->translate('Users'), + 'groups' => $this->translate('Groups'), + 'permissions' => $this->translate('Permissions') + ]; + + $this->setupFilterControl($this->view->roles, $sortAndFilterColumns, ['name']); + $this->setupLimitControl(); + $this->setupPaginationControl($this->view->roles); + $this->setupSortControl($sortAndFilterColumns, $this->view->roles, ['name']); + } + + /** + * Create a new role + * + * @TODO(el): Rename to newAction() + */ + public function addAction() + { + $role = new RoleForm(); + $role->setRedirectUrl('__CLOSE__'); + $role->setRepository(new RolesConfig()); + $role->setSubmitLabel($this->translate('Create Role')); + $role->add()->handleRequest(); + + $this->renderForm($role, $this->translate('New Role')); + } + + /** + * Update a role + * + * @TODO(el): Rename to updateAction() + */ + public function editAction() + { + $name = $this->params->getRequired('role'); + $role = new RoleForm(); + $role->setRedirectUrl('__CLOSE__'); + $role->setRepository(new RolesConfig()); + $role->setSubmitLabel($this->translate('Update Role')); + $role->edit($name); + + try { + $role->handleRequest(); + } catch (NotFoundError $e) { + $this->httpNotFound($this->translate('Role not found')); + } + + $this->renderForm($role, $this->translate('Update Role')); + } + + /** + * Remove a role + */ + public function removeAction() + { + $name = $this->params->getRequired('role'); + $role = new RoleForm(); + $role->setRedirectUrl('__CLOSE__'); + $role->setRepository(new RolesConfig()); + $role->setSubmitLabel($this->translate('Remove Role')); + $role->remove($name); + + try { + $role->handleRequest(); + } catch (NotFoundError $e) { + $this->httpNotFound($this->translate('Role not found')); + } + + $this->renderForm($role, $this->translate('Remove Role')); + } + + public function auditAction() + { + $this->createListTabs()->activate('role/audit'); + $this->view->title = t('Audit'); + + $roleName = $this->params->get('role'); + $type = $this->params->has('group') ? 'group' : 'user'; + $name = $this->params->get($type); + + $backend = null; + if ($type === 'user') { + if ($name) { + $backend = $this->params->getRequired('backend'); + } else { + $backends = $this->loadUserBackends(); + if (! empty($backends)) { + $backend = array_shift($backends)->getName(); + } + } + } + + $form = new SingleValueSearchControl(); + $form->setMetaDataNames('type', 'backend'); + $form->populate(['q' => $name, 'q-type' => $type, 'q-backend' => $backend]); + $form->setInputLabel(t('Enter user or group name')); + $form->setSubmitLabel(t('Inspect')); + $form->setSuggestionUrl(Url::fromPath( + 'role/suggest-role-member', + ['_disableLayout' => true, 'showCompact' => true] + )); + + $form->on(SingleValueSearchControl::ON_SUCCESS, function ($form) { + $type = $form->getValue('q-type') ?: 'user'; + $params = [$type => $form->getValue('q')]; + + if ($type === 'user') { + $params['backend'] = $form->getValue('q-backend'); + } + + $this->redirectNow(Url::fromPath('role/audit', $params)); + })->handleRequest(ServerRequest::fromGlobals()); + + $this->addControl($form); + + if (! $name) { + $this->addContent(Html::wantHtml(t('No user or group selected.'))); + return; + } + + if ($type === 'user') { + $header = Html::tag('h2', sprintf(t('Privilege Audit for User "%s"'), $name)); + + $user = new User($name); + $user->setAdditional('backend_name', $backend); + Auth::getInstance()->setupUser($user); + } else { + $header = Html::tag('h2', sprintf(t('Privilege Audit for Group "%s"'), $name)); + + $user = new User((string) time()); + $user->setGroups([$name]); + (new AdmissionLoader())->applyRoles($user); + } + + $chosenRole = null; + $assignedRoles = array_filter($user->getRoles(), function ($role) use ($user, &$chosenRole, $roleName) { + if (! in_array($role->getName(), $user->getAdditional('assigned_roles'), true)) { + return false; + } + + if ($role->getName() === $roleName) { + $chosenRole = $role; + } + + return true; + }); + + $this->addControl(Html::tag( + 'ul', + ['class' => 'privilege-audit-role-control'], + [ + Html::tag('li', $roleName ? null : ['class' => 'active'], new Link( + t('All roles'), + Url::fromRequest()->without('role'), + ['class' => 'button-link', 'title' => t('Show privileges of all roles')] + )), + array_map(function ($role) use ($roleName) { + return Html::tag( + 'li', + $role->getName() === $roleName ? ['class' => 'active'] : null, + new Link( + $role->getName(), + Url::fromRequest()->setParam('role', $role->getName()), + [ + 'class' => 'button-link', + 'title' => sprintf(t('Only show privileges of role %s'), $role->getName()) + ] + ) + ); + }, $assignedRoles) + ] + )); + + $this->addControl($header); + $this->addContent( + (new PrivilegeAudit($chosenRole !== null ? [$chosenRole] : $assignedRoles)) + ->addAttributes(['id' => 'role-audit']) + ); + } + + public function suggestRoleMemberAction() + { + $this->assertHttpMethod('POST'); + $requestData = $this->getRequest()->getPost(); + $limit = $this->params->get('limit', 50); + + $searchTerm = $requestData['term']['label']; + $userBackends = $this->loadUserBackends(Selectable::class); + + $suggestions = []; + while ($limit > 0 && ! empty($userBackends)) { + /** @var Repository $backend */ + $backend = array_shift($userBackends); + $query = $backend->select() + ->from('user', ['user_name']) + ->where('user_name', $searchTerm) + ->limit($limit); + + try { + $names = $query->fetchColumn(); + } catch (Exception $e) { + continue; + } + + $domain = ''; + if ($backend instanceof DomainAwareInterface) { + $domain = '@' . $backend->getDomain(); + } + + $users = []; + foreach ($names as $name) { + $users[] = [$name . $domain, [ + 'type' => 'user', + 'backend' => $backend->getName() + ]]; + } + + if (! empty($users)) { + $suggestions[] = [ + [ + t('Users'), + HtmlString::create(' '), + Html::tag('span', ['class' => 'badge'], $backend->getName()) + ], + $users + ]; + } + + $limit -= count($names); + } + + $groupBackends = $this->loadUserGroupBackends(Selectable::class); + + while ($limit > 0 && ! empty($groupBackends)) { + /** @var Repository $backend */ + $backend = array_shift($groupBackends); + $query = $backend->select() + ->from('group', ['group_name']) + ->where('group_name', $searchTerm) + ->limit($limit); + + try { + $names = $query->fetchColumn(); + } catch (Exception $e) { + continue; + } + + $groups = []; + foreach ($names as $name) { + $groups[] = [$name, ['type' => 'group']]; + } + + if (! empty($groups)) { + $suggestions[] = [ + [ + t('Groups'), + HtmlString::create(' '), + Html::tag('span', ['class' => 'badge'], $backend->getName()) + ], + $groups + ]; + } + + $limit -= count($names); + } + + if (empty($suggestions)) { + $suggestions[] = [t('Your search does not match any user or group'), []]; + } + + $this->document->add(SingleValueSearchControl::createSuggestions($suggestions)); + } + + /** + * Create the tabs to display when listing roles + */ + protected function createListTabs() + { + $tabs = $this->getTabs(); + $tabs->add( + 'role/list', + array( + 'baseTarget' => '_main', + 'label' => $this->translate('Roles'), + 'title' => $this->translate( + 'Configure roles to permit or restrict users and groups accessing Icinga Web 2' + ), + 'url' => 'role/list' + ) + ); + + $tabs->add( + 'role/audit', + [ + 'title' => $this->translate('Audit a user\'s or group\'s privileges'), + 'label' => $this->translate('Audit'), + 'url' => 'role/audit', + 'baseTarget' => '_main' + ] + ); + + if ($this->hasPermission('config/access-control/users')) { + $tabs->add( + 'user/list', + array( + 'title' => $this->translate('List users of authentication backends'), + 'label' => $this->translate('Users'), + 'url' => 'user/list' + ) + ); + } + + if ($this->hasPermission('config/access-control/groups')) { + $tabs->add( + 'group/list', + array( + 'title' => $this->translate('List groups of user group backends'), + 'label' => $this->translate('User Groups'), + 'url' => 'group/list' + ) + ); + } + + return $tabs; + } +} diff --git a/application/controllers/SearchController.php b/application/controllers/SearchController.php new file mode 100644 index 0000000..92aeabe --- /dev/null +++ b/application/controllers/SearchController.php @@ -0,0 +1,28 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Controllers; + +use Icinga\Web\Controller\ActionController; +use Icinga\Web\Widget; +use Icinga\Web\Widget\SearchDashboard; + +/** + * Search controller + */ +class SearchController extends ActionController +{ + public function indexAction() + { + $searchDashboard = new SearchDashboard(); + $searchDashboard->setUser($this->Auth()->getUser()); + $this->view->dashboard = $searchDashboard->search($this->params->get('q')); + + // NOTE: This renders the dashboard twice. Remove this once we can catch exceptions thrown in view scripts. + $this->view->dashboard->render(); + } + + public function hintAction() + { + } +} diff --git a/application/controllers/StaticController.php b/application/controllers/StaticController.php new file mode 100644 index 0000000..44a807a --- /dev/null +++ b/application/controllers/StaticController.php @@ -0,0 +1,78 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Controllers; + +use Icinga\Application\Icinga; +use Icinga\Web\Controller; +use Icinga\Web\FileCache; + +/** + * Deliver static content to clients + */ +class StaticController extends Controller +{ + /** + * Static routes don't require authentication + * + * @var bool + */ + protected $requiresAuthentication = false; + + /** + * Disable layout rendering as this controller doesn't provide any html layouts + */ + public function init() + { + $this->_helper->viewRenderer->setNoRender(true); + $this->_helper->layout()->disableLayout(); + } + + /** + * Return an image from a module's public folder + */ + public function imgAction() + { + $imgRoot = Icinga::app() + ->getModuleManager() + ->getModule($this->getParam('module_name')) + ->getBaseDir() . '/public/img/'; + + $file = $this->getParam('file'); + $filePath = realpath($imgRoot . $file); + + if ($filePath === false || substr($filePath, 0, strlen($imgRoot)) !== $imgRoot) { + $this->httpNotFound('%s does not exist', $file); + } + + if (preg_match('/\.([a-z]+)$/i', $file, $m)) { + $extension = $m[1]; + if ($extension === 'svg') { + $extension = 'svg+xml'; + } + } else { + $extension = 'fixme'; + } + + $s = stat($filePath); + $eTag = sprintf('%x-%x-%x', $s['ino'], $s['size'], (float) str_pad((string) $s['mtime'], 16, '0')); + + $this->getResponse()->setHeader( + 'Cache-Control', + 'public, max-age=1814400, stale-while-revalidate=604800', + true + ); + + if ($this->getRequest()->getServer('HTTP_IF_NONE_MATCH') === $eTag) { + $this->getResponse() + ->setHttpResponseCode(304); + } else { + $this->getResponse() + ->setHeader('ETag', $eTag) + ->setHeader('Content-Type', 'image/' . $extension, true) + ->setHeader('Last-Modified', gmdate('D, d M Y H:i:s', $s['mtime']) . ' GMT'); + + readfile($filePath); + } + } +} diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php new file mode 100644 index 0000000..dac80d3 --- /dev/null +++ b/application/controllers/UserController.php @@ -0,0 +1,374 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Controllers; + +use Exception; +use Icinga\Application\Logger; +use Icinga\Authentication\AdmissionLoader; +use Icinga\Authentication\User\DomainAwareInterface; +use Icinga\Data\DataArray\ArrayDatasource; +use Icinga\Exception\ConfigurationError; +use Icinga\Exception\NotFoundError; +use Icinga\Forms\Config\User\CreateMembershipForm; +use Icinga\Forms\Config\User\UserForm; +use Icinga\User; +use Icinga\Web\Controller\AuthBackendController; +use Icinga\Web\Form; +use Icinga\Web\Notification; +use Icinga\Web\Url; +use Icinga\Web\Widget; + +class UserController extends AuthBackendController +{ + public function init() + { + $this->view->title = $this->translate('Users'); + + parent::init(); + } + + /** + * List all users of a single backend + */ + public function listAction() + { + $this->assertPermission('config/access-control/users'); + $this->createListTabs()->activate('user/list'); + $backendNames = array_map( + function ($b) { + return $b->getName(); + }, + $this->loadUserBackends('Icinga\Data\Selectable') + ); + if (empty($backendNames)) { + return; + } + + $this->view->backendSelection = new Form(); + $this->view->backendSelection->setAttrib('class', 'backend-selection icinga-controls'); + $this->view->backendSelection->setUidDisabled(); + $this->view->backendSelection->setMethod('GET'); + $this->view->backendSelection->setTokenDisabled(); + $this->view->backendSelection->addElement( + 'select', + 'backend', + array( + 'autosubmit' => true, + 'label' => $this->translate('User Backend'), + 'multiOptions' => array_combine($backendNames, $backendNames), + 'value' => $this->params->get('backend') + ) + ); + + $backend = $this->getUserBackend($this->params->get('backend')); + if ($backend === null) { + $this->view->backend = null; + return; + } + + $query = $backend->select(array('user_name')); + + $this->view->users = $query; + $this->view->backend = $backend; + + $this->setupPaginationControl($query); + $this->setupFilterControl($query); + $this->setupLimitControl(); + $this->setupSortControl( + array( + 'user_name' => $this->translate('Username'), + 'is_active' => $this->translate('Active'), + 'created_at' => $this->translate('Created at'), + 'last_modified' => $this->translate('Last modified') + ), + $query + ); + } + + /** + * Show a user + */ + public function showAction() + { + $this->assertPermission('config/access-control/users'); + $userName = $this->params->getRequired('user'); + $backend = $this->getUserBackend($this->params->getRequired('backend')); + + $user = $backend->select(array( + 'user_name', + 'is_active', + 'created_at', + 'last_modified' + ))->where('user_name', $userName)->fetchRow(); + if ($user === false) { + $this->httpNotFound(sprintf($this->translate('User "%s" not found'), $userName)); + } + + $userObj = new User($userName); + if ($backend instanceof DomainAwareInterface) { + $userObj->setDomain($backend->getDomain()); + } + + $memberships = $this->loadMemberships($userObj)->select(); + + $this->setupFilterControl( + $memberships, + array('group_name' => t('User Group')), + array('group'), + array('user') + ); + $this->setupPaginationControl($memberships); + $this->setupLimitControl(); + $this->setupSortControl( + array( + 'group_name' => $this->translate('Group') + ), + $memberships + ); + + if ($this->hasPermission('config/access-control/groups')) { + $extensibleBackends = $this->loadUserGroupBackends('Icinga\Data\Extensible'); + $this->view->showCreateMembershipLink = ! empty($extensibleBackends); + } else { + $this->view->showCreateMembershipLink = false; + } + + $this->view->user = $user; + $this->view->backend = $backend; + $this->view->memberships = $memberships; + $this->createShowTabs($backend->getName(), $userName)->activate('user/show'); + + if ($this->hasPermission('config/access-control/groups')) { + $removeForm = new Form(); + $removeForm->setUidDisabled(); + $removeForm->setAttrib('class', 'inline'); + $removeForm->addElement('hidden', 'user_name', array( + 'isArray' => true, + 'value' => $userName, + 'decorators' => array('ViewHelper') + )); + $removeForm->addElement('hidden', 'redirect', array( + 'value' => Url::fromPath('user/show', array( + 'backend' => $backend->getName(), + 'user' => $userName + )), + 'decorators' => array('ViewHelper') + )); + $removeForm->addElement('button', 'btn_submit', array( + 'escape' => false, + 'type' => 'submit', + 'class' => 'link-button spinner', + 'value' => 'btn_submit', + 'decorators' => array('ViewHelper'), + 'label' => $this->view->icon('trash'), + 'title' => $this->translate('Cancel this membership') + )); + $this->view->removeForm = $removeForm; + } + + $admissionLoader = new AdmissionLoader(); + $admissionLoader->applyRoles($userObj); + $this->view->userObj = $userObj; + $this->view->allowedToEditRoles = $this->hasPermission('config/access-control/groups'); + } + + /** + * Add a user + */ + public function addAction() + { + $this->assertPermission('config/access-control/users'); + $backend = $this->getUserBackend($this->params->getRequired('backend'), 'Icinga\Data\Extensible'); + $form = new UserForm(); + $form->setRedirectUrl(Url::fromPath('user/list', array('backend' => $backend->getName()))); + $form->setRepository($backend); + $form->add()->handleRequest(); + + $this->renderForm($form, $this->translate('New User')); + } + + /** + * Edit a user + */ + public function editAction() + { + $this->assertPermission('config/access-control/users'); + $userName = $this->params->getRequired('user'); + $backend = $this->getUserBackend($this->params->getRequired('backend'), 'Icinga\Data\Updatable'); + + $form = new UserForm(); + $form->setRedirectUrl(Url::fromPath('user/show', array('backend' => $backend->getName(), 'user' => $userName))); + $form->setRepository($backend); + + try { + $form->edit($userName)->handleRequest(); + } catch (NotFoundError $_) { + $this->httpNotFound(sprintf($this->translate('User "%s" not found'), $userName)); + } + + $this->renderForm($form, $this->translate('Update User')); + } + + /** + * Remove a user + */ + public function removeAction() + { + $this->assertPermission('config/access-control/users'); + $userName = $this->params->getRequired('user'); + $backend = $this->getUserBackend($this->params->getRequired('backend'), 'Icinga\Data\Reducible'); + + $form = new UserForm(); + $form->setRedirectUrl(Url::fromPath('user/list', array('backend' => $backend->getName()))); + $form->setRepository($backend); + + try { + $form->remove($userName)->handleRequest(); + } catch (NotFoundError $_) { + $this->httpNotFound(sprintf($this->translate('User "%s" not found'), $userName)); + } + + $this->renderForm($form, $this->translate('Remove User')); + } + + /** + * Create a membership for a user + */ + public function createmembershipAction() + { + $this->assertPermission('config/access-control/groups'); + $userName = $this->params->getRequired('user'); + $backend = $this->getUserBackend($this->params->getRequired('backend')); + + if ($backend->select()->where('user_name', $userName)->count() === 0) { + $this->httpNotFound(sprintf($this->translate('User "%s" not found'), $userName)); + } + + $backends = $this->loadUserGroupBackends('Icinga\Data\Extensible'); + if (empty($backends)) { + throw new ConfigurationError($this->translate( + 'You\'ll need to configure at least one user group backend first that allows to create new memberships' + )); + } + + $form = new CreateMembershipForm(); + $form->setBackends($backends) + ->setUsername($userName) + ->setRedirectUrl(Url::fromPath('user/show', array('backend' => $backend->getName(), 'user' => $userName))) + ->handleRequest(); + + $this->renderForm($form, $this->translate('Create New Membership')); + } + + /** + * Fetch and return the given user's groups from all user group backends + * + * @param User $user + * + * @return ArrayDatasource + */ + protected function loadMemberships(User $user) + { + $groups = $alreadySeen = array(); + foreach ($this->loadUserGroupBackends() as $backend) { + try { + foreach ($backend->getMemberships($user) as $groupName) { + if (array_key_exists($groupName, $alreadySeen)) { + continue; // Ignore duplicate memberships + } + + $alreadySeen[$groupName] = null; + $groups[] = (object) array( + 'group_name' => $groupName, + 'group' => $groupName, + 'backend' => $backend + ); + } + } catch (Exception $e) { + Logger::error($e); + Notification::warning(sprintf( + $this->translate('Failed to fetch memberships from backend %s. Please check your log'), + $backend->getName() + )); + } + } + + return new ArrayDatasource($groups); + } + + /** + * Create the tabs to display when showing a user + * + * @param string $backendName + * @param string $userName + */ + protected function createShowTabs($backendName, $userName) + { + $tabs = $this->getTabs(); + $tabs->add( + 'user/show', + array( + 'title' => sprintf($this->translate('Show user %s'), $userName), + 'label' => $this->translate('User'), + 'url' => Url::fromPath('user/show', array('backend' => $backendName, 'user' => $userName)) + ) + ); + + return $tabs; + } + + /** + * Create the tabs to display when listing users + */ + protected function createListTabs() + { + $tabs = $this->getTabs(); + + if ($this->hasPermission('config/access-control/roles')) { + $tabs->add( + 'role/list', + array( + 'baseTarget' => '_main', + 'label' => $this->translate('Roles'), + 'title' => $this->translate( + 'Configure roles to permit or restrict users and groups accessing Icinga Web 2' + ), + 'url' => 'role/list' + ) + ); + + $tabs->add( + 'role/audit', + [ + 'title' => $this->translate('Audit a user\'s or group\'s privileges'), + 'label' => $this->translate('Audit'), + 'url' => 'role/audit', + 'baseTarget' => '_main' + ] + ); + } + + $tabs->add( + 'user/list', + array( + 'title' => $this->translate('List users of authentication backends'), + 'label' => $this->translate('Users'), + 'url' => 'user/list' + ) + ); + + if ($this->hasPermission('config/access-control/groups')) { + $tabs->add( + 'group/list', + array( + 'title' => $this->translate('List groups of user group backends'), + 'label' => $this->translate('User Groups'), + 'url' => 'group/list' + ) + ); + } + + return $tabs; + } +} diff --git a/application/controllers/UsergroupbackendController.php b/application/controllers/UsergroupbackendController.php new file mode 100644 index 0000000..a96ab75 --- /dev/null +++ b/application/controllers/UsergroupbackendController.php @@ -0,0 +1,133 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Controllers; + +use Exception; +use Icinga\Application\Config; +use Icinga\Exception\NotFoundError; +use Icinga\Forms\Config\UserGroup\UserGroupBackendForm; +use Icinga\Forms\ConfirmRemovalForm; +use Icinga\Web\Controller; +use Icinga\Web\Notification; + +/** + * Controller to configure user group backends + */ +class UsergroupbackendController extends Controller +{ + /** + * Initialize this controller + */ + public function init() + { + $this->assertPermission('config/access-control/users'); + } + + /** + * Redirect to this controller's list action + */ + public function indexAction() + { + $this->redirectNow('config/userbackend'); + } + + /** + * Create a new user group backend + */ + public function createAction() + { + $form = new UserGroupBackendForm(); + $form->setRedirectUrl('config/userbackend'); + $form->addDescription($this->translate('Create a new backend to associate users and groups with.')); + $form->setIniConfig(Config::app('groups')); + $form->setOnSuccess(function (UserGroupBackendForm $form) { + try { + $form->add($form::transformEmptyValuesToNull($form->getValues())); + } catch (Exception $e) { + $form->error($e->getMessage()); + return false; + } + + if ($form->save()) { + Notification::success(t('User group backend successfully created')); + return true; + } + + return false; + }); + $form->handleRequest(); + + $this->view->title = $this->translate('Authentication'); + $this->renderForm($form, $this->translate('New User Group Backend')); + } + + /** + * Edit an user group backend + */ + public function editAction() + { + $backendName = $this->params->getRequired('backend'); + + $form = new UserGroupBackendForm(); + $form->setRedirectUrl('config/userbackend'); + $form->setIniConfig(Config::app('groups')); + $form->setOnSuccess(function (UserGroupBackendForm $form) use ($backendName) { + try { + $form->edit($backendName, $form::transformEmptyValuesToNull($form->getValues())); + } catch (Exception $e) { + $form->error($e->getMessage()); + return false; + } + + if ($form->save()) { + Notification::success(sprintf(t('User group backend "%s" successfully updated'), $backendName)); + return true; + } + + return false; + }); + + try { + $form->load($backendName); + $form->handleRequest(); + } catch (NotFoundError $_) { + $this->httpNotFound(sprintf($this->translate('User group backend "%s" not found'), $backendName)); + } + + $this->view->title = $this->translate('Authentication'); + $this->renderForm($form, $this->translate('Update User Group Backend')); + } + + /** + * Remove a user group backend + */ + public function removeAction() + { + $backendName = $this->params->getRequired('backend'); + + $backendForm = new UserGroupBackendForm(); + $backendForm->setIniConfig(Config::app('groups')); + $form = new ConfirmRemovalForm(); + $form->setRedirectUrl('config/userbackend'); + $form->setOnSuccess(function (ConfirmRemovalForm $form) use ($backendName, $backendForm) { + try { + $backendForm->delete($backendName); + } catch (Exception $e) { + $form->error($e->getMessage()); + return false; + } + + if ($backendForm->save()) { + Notification::success(sprintf(t('User group backend "%s" successfully removed'), $backendName)); + return true; + } + + return false; + }); + $form->handleRequest(); + + $this->view->title = $this->translate('Authentication'); + $this->renderForm($form, $this->translate('Remove User Group Backend')); + } +} |