diff --git a/application/controllers/ContactGroupController.php b/application/controllers/ContactGroupController.php index aec7a38e9..a22224389 100644 --- a/application/controllers/ContactGroupController.php +++ b/application/controllers/ContactGroupController.php @@ -24,7 +24,7 @@ class ContactGroupController extends CompatController { public function init(): void { - $this->assertPermission('notifications/config/contact-groups'); + $this->assertPermission('notifications/config/contacts'); } public function indexAction(): void diff --git a/application/controllers/ContactGroupsController.php b/application/controllers/ContactGroupsController.php index 0917c9ffa..3c0f18581 100644 --- a/application/controllers/ContactGroupsController.php +++ b/application/controllers/ContactGroupsController.php @@ -4,6 +4,7 @@ namespace Icinga\Module\Notifications\Controllers; +use Icinga\Module\Notifications\Common\ConfigurationTabs; use Icinga\Module\Notifications\Common\Database; use Icinga\Module\Notifications\Common\Links; use Icinga\Module\Notifications\Forms\ContactGroupForm; @@ -28,10 +29,10 @@ use ipl\Web\Layout\MinimalItemLayout; use ipl\Web\Widget\ActionLink; use ipl\Web\Widget\ButtonLink; -use ipl\Web\Widget\Tabs; class ContactGroupsController extends CompatController { + use ConfigurationTabs; use SearchControls; /** @var Filter\Rule Filter from query string parameters */ @@ -39,7 +40,7 @@ class ContactGroupsController extends CompatController public function init(): void { - $this->assertPermission('notifications/config/contact-groups'); + $this->assertPermission('notifications/config/contacts'); } public function indexAction(): void @@ -96,15 +97,23 @@ public function indexAction(): void if (Channel::on(Database::get())->columns([new Expression('1')])->limit(1)->first() === null) { $addButton->disable($this->translate('A channel is required to add a contact group')); - $emptyStateMessage = TemplateString::create( - // translators: %1$s will be replaced by a line break - $this->translate( - 'No contact groups found.%1$sTo add new contact group, please {{#link}}configure a' - . ' Channel{{/link}} first.%1$sOnce done, you should proceed by creating your first contact.' - ), - ['link' => (new ActionLink(null, Links::channelAdd()))->setBaseTarget('_next')], - [HtmlString::create('
')] - ); + if ($this->Auth()->hasPermission('config/modules')) { + $emptyStateMessage = TemplateString::create( + // translators: %1$s will be replaced by a line break + $this->translate( + 'No contact groups found.%1$s' + . 'To add new contact group, please {{#link}}configure a Channel{{/link}} first.%1$s' + . 'Once done, you should proceed by creating your first contact.' + ), + ['link' => (new ActionLink(null, Links::channelAdd()))->setBaseTarget('_next')], + [HtmlString::create('
')] + ); + } else { + $emptyStateMessage = $this->translate( + 'No contact groups found. To add a new contact group, a channel is required.' + . ' Please contact your system administrator.' + ); + } } else { $emptyStateMessage = TemplateString::create( $this->translate( @@ -195,28 +204,6 @@ public function suggestMemberAction(): void $this->getDocument()->addHtml($members); } - public function getTabs(): Tabs - { - return parent::getTabs() - ->add('schedules', [ - 'label' => $this->translate('Schedules'), - 'url' => Links::schedules(), - 'baseTarget' => '_main' - ])->add('event-rules', [ - 'label' => $this->translate('Event Rules'), - 'url' => Links::eventRules(), - 'baseTarget' => '_main' - ])->add('contacts', [ - 'label' => $this->translate('Contacts'), - 'url' => Links::contacts(), - 'baseTarget' => '_main' - ])->add('contact-groups', [ - 'label' => $this->translate('Contact Groups'), - 'url' => Links::contactGroups(), - 'baseTarget' => '_main' - ]); - } - /** * Get the filter created from query string parameters * diff --git a/application/controllers/ContactsController.php b/application/controllers/ContactsController.php index 2fd3640a2..2e427b7cb 100644 --- a/application/controllers/ContactsController.php +++ b/application/controllers/ContactsController.php @@ -4,6 +4,7 @@ namespace Icinga\Module\Notifications\Controllers; +use Icinga\Module\Notifications\Common\ConfigurationTabs; use Icinga\Module\Notifications\Common\Links; use Icinga\Module\Notifications\Model\Channel; use Icinga\Module\Notifications\View\ContactRenderer; @@ -29,6 +30,7 @@ class ContactsController extends CompatController { + use ConfigurationTabs; use SearchControls; /** @var Connection */ @@ -96,12 +98,19 @@ public function indexAction() if (Channel::on($this->db)->columns([new Expression('1')])->limit(1)->first() === null) { $addButton->disable($this->translate('A channel is required to add a contact')); - $emptyStateMessage = TemplateString::create( - $this->translate( - 'No contacts found. To add a new contact, please {{#link}}configure a Channel{{/link}} first.' - ), - ['link' => (new ActionLink(null, Links::channelAdd()))->setBaseTarget('_next')] - ); + if ($this->Auth()->hasPermission('config/modules')) { + $emptyStateMessage = TemplateString::create( + $this->translate( + 'No contacts found. To add a new contact, please {{#link}}configure a Channel{{/link}} first.' + ), + ['link' => (new ActionLink(null, Links::channelAdd()))->setBaseTarget('_next')] + ); + } else { + $emptyStateMessage = $this->translate( + 'No contacts found. To add a new contact, a channel is required.' + . ' Please contact your system administrator.' + ); + } } $this->addContent($addButton); @@ -171,30 +180,4 @@ protected function getFilter(): Filter\Rule return $this->filter; } - - public function getTabs() - { - if ($this->getRequest()->getActionName() === 'index') { - return parent::getTabs() - ->add('schedules', [ - 'label' => $this->translate('Schedules'), - 'url' => Links::schedules(), - 'baseTarget' => '_main' - ])->add('event-rules', [ - 'label' => $this->translate('Event Rules'), - 'url' => Links::eventRules(), - 'baseTarget' => '_main' - ])->add('contacts', [ - 'label' => $this->translate('Contacts'), - 'url' => Links::contacts(), - 'baseTarget' => '_main' - ])->add('contact-groups', [ - 'label' => $this->translate('Contact Groups'), - 'url' => Links::contactGroups(), - 'baseTarget' => '_main' - ]); - } - - return parent::getTabs(); - } } diff --git a/application/controllers/EventRuleController.php b/application/controllers/EventRuleController.php index 9f61cffc7..d15526751 100644 --- a/application/controllers/EventRuleController.php +++ b/application/controllers/EventRuleController.php @@ -39,7 +39,7 @@ class EventRuleController extends CompatController public function init(): void { - $this->assertPermission('notifications/config/event-rule'); + $this->assertPermission('notifications/config/event-rules'); $this->session = Session::getSession()->getNamespace('notifications.event-rule'); } diff --git a/application/controllers/EventRulesController.php b/application/controllers/EventRulesController.php index 70e1db13b..07c938003 100644 --- a/application/controllers/EventRulesController.php +++ b/application/controllers/EventRulesController.php @@ -4,6 +4,7 @@ namespace Icinga\Module\Notifications\Controllers; +use Icinga\Module\Notifications\Common\ConfigurationTabs; use Icinga\Module\Notifications\Common\Database; use Icinga\Module\Notifications\Common\Links; use Icinga\Module\Notifications\Forms\EventRuleForm; @@ -28,6 +29,7 @@ class EventRulesController extends CompatController { + use ConfigurationTabs; use SearchControls; public function init() @@ -159,30 +161,4 @@ public function searchEditorAction(): void $this->getDocument()->add($editor); $this->setTitle($this->translate('Adjust Filter')); } - - public function getTabs() - { - if ($this->getRequest()->getActionName() === 'index') { - return parent::getTabs() - ->add('schedules', [ - 'label' => $this->translate('Schedules'), - 'url' => Links::schedules(), - 'baseTarget' => '_main' - ])->add('event-rules', [ - 'label' => $this->translate('Event Rules'), - 'url' => Links::eventRules(), - 'baseTarget' => '_main' - ])->add('contacts', [ - 'label' => $this->translate('Contacts'), - 'url' => Links::contacts(), - 'baseTarget' => '_main' - ])->add('contact-groups', [ - 'label' => $this->translate('Contact Groups'), - 'url' => Links::contactGroups(), - 'baseTarget' => '_main' - ]); - } - - return parent::getTabs(); - } } diff --git a/application/controllers/ScheduleController.php b/application/controllers/ScheduleController.php index d9e0b1204..177f9a028 100644 --- a/application/controllers/ScheduleController.php +++ b/application/controllers/ScheduleController.php @@ -30,6 +30,8 @@ class ScheduleController extends CompatController public function init(): void { + $this->assertPermission('notifications/config/schedules'); + parent::init(); $this->session = Session::getSession()->getNamespace('notifications.schedule'); diff --git a/application/controllers/SchedulesController.php b/application/controllers/SchedulesController.php index a9c212486..e07d079d2 100644 --- a/application/controllers/SchedulesController.php +++ b/application/controllers/SchedulesController.php @@ -4,6 +4,7 @@ namespace Icinga\Module\Notifications\Controllers; +use Icinga\Module\Notifications\Common\ConfigurationTabs; use Icinga\Module\Notifications\Common\Database; use Icinga\Module\Notifications\Common\Links; use Icinga\Module\Notifications\Model\Schedule; @@ -17,15 +18,20 @@ use ipl\Web\Control\SortControl; use ipl\Web\Filter\QueryString; use ipl\Web\Widget\ButtonLink; -use ipl\Web\Widget\Tabs; class SchedulesController extends CompatController { + use ConfigurationTabs; use SearchControls; /** @var Filter\Rule Filter from query string parameters */ private $filter; + public function init(): void + { + $this->assertPermission('notifications/config/schedules'); + } + public function indexAction(): void { $schedules = Schedule::on(Database::get()); @@ -102,28 +108,6 @@ public function searchEditorAction(): void $this->setTitle(t('Adjust Filter')); } - public function getTabs(): Tabs - { - return parent::getTabs() - ->add('schedules', [ - 'label' => $this->translate('Schedules'), - 'url' => Links::schedules(), - 'baseTarget' => '_main' - ])->add('event-rules', [ - 'label' => $this->translate('Event Rules'), - 'url' => Links::eventRules(), - 'baseTarget' => '_main' - ])->add('contacts', [ - 'label' => $this->translate('Contacts'), - 'url' => Links::contacts(), - 'baseTarget' => '_main' - ])->add('contact-groups', [ - 'label' => $this->translate('Contact Groups'), - 'url' => Links::contactGroups(), - 'baseTarget' => '_main' - ]); - } - /** * Get the filter created from query string parameters * diff --git a/configuration.php b/configuration.php index 439e09da3..a26fd7014 100644 --- a/configuration.php +++ b/configuration.php @@ -3,6 +3,7 @@ /* Icinga Notifications Web | (c) 2023 Icinga GmbH | GPLv2 */ use Icinga\Application\Modules\Module; +use Icinga\Authentication\Auth; /** @var Module $this */ @@ -14,14 +15,30 @@ ] ); -$section->add( - N_('Configuration'), - [ - 'icon' => 'wrench', - 'description' => $this->translate('Configuration'), - 'url' => 'notifications/schedules' - ] -); +$auth = Auth::getInstance(); +$authenticated = $auth->getUser() !== null; + +$configLandingPage = null; +if ($authenticated) { + if ($auth->hasPermission('notifications/config/schedules')) { + $configLandingPage = 'notifications/schedules'; + } elseif ($auth->hasPermission('notifications/config/event-rules')) { + $configLandingPage = 'notifications/event-rules'; + } elseif ($auth->hasPermission('notifications/config/contacts')) { + $configLandingPage = 'notifications/contacts'; + } +} + +if ($configLandingPage !== null) { + $section->add( + N_('Configuration'), + [ + 'icon' => 'wrench', + 'description' => $this->translate('Configuration'), + 'url' => $configLandingPage + ] + ); +} $section->add( N_('Events'), @@ -32,14 +49,24 @@ ] ); +$this->providePermission( + 'notifications/config/schedules', + $this->translate('Allow to configure schedules') +); + $this->providePermission( 'notifications/config/event-rules', $this->translate('Allow to configure event rules') ); $this->providePermission( - 'notifications/config/contact-groups', - $this->translate('Allow to configure contact groups') + 'notifications/config/contacts', + $this->translate('Allow to configure contacts and contact groups') +); + +$this->providePermission( + 'notifications/view/contacts', + $this->translate('Allow to view contacts') ); $this->providePermission( @@ -47,10 +74,10 @@ $this->translate('Allow to modify configuration via API') ); -$this->provideRestriction( - 'notifications/filter/objects', - $this->translate('Restrict access to the objects that match the filter') -); +//$this->provideRestriction( +// 'notifications/filter/objects', +// $this->translate('Restrict access to the objects that match the filter') +//); $this->provideConfigTab( 'database', diff --git a/doc/03-Configuration.md b/doc/03-Configuration.md index d0da5e9be..e4b4b4ef2 100644 --- a/doc/03-Configuration.md +++ b/doc/03-Configuration.md @@ -18,6 +18,24 @@ If you just installed Icinga Notifications Web, remember to activate it on your +## Access Control + +!!! warning + + Authorization mechanics in the current release are not fully functional. For example, restricting users to certain + objects is not supported. Do not grant users access to the module unless you are sure they are authorized to see + **all** events and incidents. + +### Permissions + +| Permission | Description | +|----------------------------------|------------------------------------------------| +| notifications/config/schedules | Allow to configure schedules | +| notifications/config/event-rules | Allow to configure event rules | +| notifications/config/contacts | Allow to configure contacts and contact groups | +| notifications/view/contacts | Allow to view contacts | +| notifications/api | Allow to modify configuration via API | + ## Database Configuration Connection configuration for the database, which both, diff --git a/library/Notifications/Common/Auth.php b/library/Notifications/Common/Auth.php index f718d7ebe..c3a56e377 100644 --- a/library/Notifications/Common/Auth.php +++ b/library/Notifications/Common/Auth.php @@ -26,6 +26,10 @@ public function getAuth(): IcingaAuth */ public function applyRestrictions(Query $query): void { + // TODO: Since the recent integration rewrite, restriction support does not work anymore as expected. + // Will be reworked with the next release. + return; + /** @var User $user */ $user = $this->getAuth()->getUser(); if ($user->isUnrestricted()) { diff --git a/library/Notifications/Common/ConfigurationTabs.php b/library/Notifications/Common/ConfigurationTabs.php new file mode 100644 index 000000000..c7771c3fc --- /dev/null +++ b/library/Notifications/Common/ConfigurationTabs.php @@ -0,0 +1,55 @@ +getRequest()->getActionName() === 'index') { + if ($this->Auth()->hasPermission('notifications/config/schedules')) { + $tabs->add('schedules', [ + 'label' => $this->translate('Schedules'), + 'url' => Links::schedules(), + 'baseTarget' => '_main' + ]); + } + + if ($this->Auth()->hasPermission('notifications/config/event-rules')) { + $tabs->add('event-rules', [ + 'label' => $this->translate('Event Rules'), + 'url' => Links::eventRules(), + 'baseTarget' => '_main' + ]); + } + + if ($this->Auth()->hasPermission('notifications/config/contacts')) { + $tabs->add('contacts', [ + 'label' => $this->translate('Contacts'), + 'url' => Links::contacts(), + 'baseTarget' => '_main' + ])->add('contact-groups', [ + 'label' => $this->translate('Contact Groups'), + 'url' => Links::contactGroups(), + 'baseTarget' => '_main' + ]); + } + } + + return $tabs; + } +} diff --git a/library/Notifications/View/IncidentContactRenderer.php b/library/Notifications/View/IncidentContactRenderer.php index 73b043e60..f6324d664 100644 --- a/library/Notifications/View/IncidentContactRenderer.php +++ b/library/Notifications/View/IncidentContactRenderer.php @@ -4,11 +4,13 @@ namespace Icinga\Module\Notifications\View; +use Icinga\Module\Notifications\Common\Auth; use Icinga\Module\Notifications\Common\Icons; use Icinga\Module\Notifications\Common\Links; use Icinga\Module\Notifications\Model\IncidentContact; use ipl\Html\Attributes; use ipl\Html\HtmlDocument; +use ipl\Html\HtmlElement; use ipl\Html\Text; use ipl\I18n\Translation; use ipl\Web\Common\ItemRenderer; @@ -20,6 +22,23 @@ class IncidentContactRenderer implements ItemRenderer { use Translation; + /** @var bool Whether the rendered item should not include a link to the contact */ + private bool $disableContactLink = false; + + /** + * Set whether the rendered item should not include a link to the contact + * + * @param bool $disableLink + * + * @return $this + */ + public function disableContactLink(bool $disableLink): static + { + $this->disableContactLink = $disableLink; + + return $this; + } + public function assembleAttributes($item, Attributes $attributes, string $layout): void { $attributes->get('class')->addValue('incident-contact'); @@ -32,7 +51,15 @@ public function assembleVisual($item, HtmlDocument $visual, string $layout): voi public function assembleTitle($item, HtmlDocument $title, string $layout): void { - $title->addHtml(new Link($item->full_name, Links::contact($item->id), ['class' => 'subject'])); + if (! $this->disableContactLink) { + $title->addHtml(new Link($item->full_name, Links::contact($item->id), ['class' => 'subject'])); + } else { + $title->addHtml(new HtmlElement( + 'span', + Attributes::create(['class' => 'subject']), + Text::create($item->full_name) + )); + } if ($item->role === 'manager') { $title->addHtml(new Text($this->translate('manages this incident'))); diff --git a/library/Notifications/Widget/Detail/IncidentDetail.php b/library/Notifications/Widget/Detail/IncidentDetail.php index 57814a945..944e7d5a8 100644 --- a/library/Notifications/Widget/Detail/IncidentDetail.php +++ b/library/Notifications/Widget/Detail/IncidentDetail.php @@ -7,6 +7,7 @@ use ArrayIterator; use Icinga\Module\Icingadb\Model\CustomvarFlat; use Icinga\Module\Icingadb\Widget\Detail\CustomVarTable; +use Icinga\Module\Notifications\Common\Auth; use Icinga\Module\Notifications\Hook\ObjectsRendererHook; use Icinga\Module\Notifications\Model\Behavior\IcingaCustomVars; use Icinga\Module\Notifications\Model\Incident; @@ -26,6 +27,7 @@ class IncidentDetail extends BaseHtmlElement { + use Auth; use Translation; /** @var Incident */ @@ -58,10 +60,14 @@ protected function createContacts() $contacts[] = $contact; } + $disableContactLink = ! $this->getAuth()->hasPermission('notifications/view/contacts') + || ! $this->getAuth()->hasPermission('notifications/config/contacts'); + return [ Html::tag('h2', t('Subscribers')), - (new ObjectList($contacts, new IncidentContactRenderer())) + (new ObjectList($contacts, (new IncidentContactRenderer())->disableContactLink($disableContactLink))) ->setItemLayoutClass(MinimalItemLayout::class) + ->setDetailActionsDisabled($disableContactLink) ]; } diff --git a/library/Notifications/Widget/ItemList/ContactGroupListItem.php b/library/Notifications/Widget/ItemList/ContactGroupListItem.php deleted file mode 100644 index 3c6a0a24d..000000000 --- a/library/Notifications/Widget/ItemList/ContactGroupListItem.php +++ /dev/null @@ -1,52 +0,0 @@ -getAttributes()->set('data-action-item', true); - } - - protected function assembleVisual(BaseHtmlElement $visual): void - { - $visual->addHtml(new HtmlElement( - 'div', - Attributes::create(['class' => 'contact-ball']), - Text::create(grapheme_substr($this->item->name, 0, 1)) - )); - } - - protected function assembleMain(BaseHtmlElement $main): void - { - $main->addHtml($this->createHeader()); - } - - protected function assembleHeader(BaseHtmlElement $header): void - { - $header->addHtml($this->createTitle()); - } - - protected function assembleTitle(BaseHtmlElement $title): void - { - $title->addHtml(new Link($this->item->name, Links::contactGroup($this->item->id), ['class' => 'subject'])); - } -} diff --git a/library/Notifications/Widget/ItemList/ContactListItem.php b/library/Notifications/Widget/ItemList/ContactListItem.php deleted file mode 100644 index fb2da107d..000000000 --- a/library/Notifications/Widget/ItemList/ContactListItem.php +++ /dev/null @@ -1,62 +0,0 @@ -getAttributes() - ->set('data-action-item', true); - } - - protected function assembleVisual(BaseHtmlElement $visual): void - { - $visual->addHtml(new HtmlElement( - 'div', - Attributes::create(['class' => 'contact-ball']), - Text::create(grapheme_substr($this->item->full_name, 0, 1)) - )); - } - - protected function assembleTitle(BaseHtmlElement $title): void - { - $title->addHtml(new Link( - $this->item->full_name, - Url::fromPath('notifications/contact', ['id' => $this->item->id]), - ['class' => 'subject'] - )); - } - - protected function assembleHeader(BaseHtmlElement $header): void - { - $header->add($this->createTitle()); - } - - protected function assembleMain(BaseHtmlElement $main): void - { - $main->add($this->createHeader()); - - $main->add($this->createFooter()); - } -}