diff --git a/application/controllers/ReportController.php b/application/controllers/ReportController.php index 8371382c..556aa0b9 100644 --- a/application/controllers/ReportController.php +++ b/application/controllers/ReportController.php @@ -35,9 +35,11 @@ public function init() $report = Model\Report::on($this->getDb()) ->with(['timeframe']) - ->filter(Filter::equal('id', $reportId)) - ->first(); + ->filter(Filter::equal('id', $reportId)); + $this->applyRestrictions($report); + + $report = $report->first(); if ($report === null) { $this->httpNotFound($this->translate('Report not found')); } diff --git a/application/controllers/ReportsController.php b/application/controllers/ReportsController.php index f1c60e61..5af8a487 100644 --- a/application/controllers/ReportsController.php +++ b/application/controllers/ReportsController.php @@ -4,6 +4,7 @@ namespace Icinga\Module\Reporting\Controllers; +use Icinga\Authentication\Auth as IcingaAuth; use Icinga\Module\Icingadb\ProvidedHook\Reporting\HostSlaReport; use Icinga\Module\Icingadb\ProvidedHook\Reporting\ServiceSlaReport; use Icinga\Module\Reporting\Database; @@ -12,6 +13,7 @@ use Icinga\Module\Reporting\Web\Forms\ReportForm; use Icinga\Module\Reporting\Web\ReportsTimeframesAndTemplatesTabs; use ipl\Html\Html; +use ipl\Stdlib\Filter; use ipl\Web\Url; use ipl\Web\Widget\ButtonLink; use ipl\Web\Widget\Icon; @@ -27,15 +29,44 @@ public function indexAction() $this->createTabs()->activate('reports'); if ($this->hasPermission('reporting/reports')) { - $this->addControl(new ButtonLink( - $this->translate('New Report'), - Url::fromPath('reporting/reports/new'), - 'plus', - [ - 'data-icinga-modal' => true, - 'data-no-icinga-ajax' => true - ] - )); + $canCreate = true; + $report = ['report.author' => $this->auth->getUser()->getUsername()]; + $restrictions = IcingaAuth::getInstance()->getRestrictions('reporting/reports'); + foreach ($restrictions as $restriction) { + $this->parseRestriction( + $restriction, + 'reporting/reports', + function (Filter\Condition $condition) use (&$canCreate, $report) { + if ($condition->getColumn() != 'report.author') { + // Only filters like `report.author!=$user.local_name$` can fully prevent the current user + // from creating his own reports. + return; + } + + if (! $canCreate || Filter::match($condition, $report)) { + return; + } + + $canCreate = false; + } + ); + + if (! $canCreate) { + break; + } + } + + if ($canCreate) { + $this->addControl(new ButtonLink( + $this->translate('New Report'), + Url::fromPath('reporting/reports/new'), + 'plus', + [ + 'data-icinga-modal' => true, + 'data-no-icinga-ajax' => true + ] + )); + } } $tableRows = []; @@ -43,6 +74,8 @@ public function indexAction() $reports = Report::on($this->getDb()) ->withColumns(['report.timeframe.name']); + $this->applyRestrictions($reports); + $sortControl = $this->createSortControl( $reports, [ @@ -64,16 +97,16 @@ public function indexAction() Html::tag('td', null, $report->timeframe->name), Html::tag('td', null, $report->ctime->format('Y-m-d H:i')), Html::tag('td', null, $report->mtime->format('Y-m-d H:i')), - Html::tag('td', ['class' => 'icon-col'], [ - new Link( + ! $this->hasPermission('reporting/reports') + ? null + : Html::tag('td', ['class' => 'icon-col'], new Link( new Icon('edit'), Url::fromPath('reporting/report/edit', ['id' => $report->id]), [ 'data-icinga-modal' => true, 'data-no-icinga-ajax' => true ] - ) - ]) + )) ]); } diff --git a/configuration.php b/configuration.php index b0e2fcac..07bbb97c 100644 --- a/configuration.php +++ b/configuration.php @@ -50,4 +50,9 @@ 'reporting/timeframes', $this->translate('Allow managing timeframes') ); + + $this->provideRestriction( + 'reporting/reports', + $this->translate('Restrict access to the reports that match the provided filter') + ); } diff --git a/doc/03-Configuration.md b/doc/03-Configuration.md index f06481ce..c9cfb6b0 100644 --- a/doc/03-Configuration.md +++ b/doc/03-Configuration.md @@ -31,3 +31,16 @@ reporting/reports | Reports (create, edit, delete) reporting/schedules | Schedules (create, edit, delete) reporting/templates | Templates (create, edit, delete) reporting/timeframes | Timeframes (create, edit, delete) + +## Restrictions + +Icinga Reporting currently provides a single restriction that can be used to limit users to a specific set of reports, +while having the `reporting/reports` permission. + +> **Note:** +> +> Filters from multiple roles will expand the available access. + +| Name | Description | +|-------------------|---------------------------------------------------------------| +| reporting/reports | Restrict access to the reports that match the provided filter | diff --git a/library/Reporting/Common/Auth.php b/library/Reporting/Common/Auth.php new file mode 100644 index 00000000..92bc0ae4 --- /dev/null +++ b/library/Reporting/Common/Auth.php @@ -0,0 +1,73 @@ +getRestrictions('reporting/reports'); + + $queryFilter = Filter::any(); + foreach ($restrictions as $restriction) { + $queryFilter->add($this->parseRestriction($restriction, 'reporting/reports')); + } + + $query->filter($queryFilter); + } + + /** + * Parse the query string of the given restriction + * + * @param string $queryString + * @param string $restriction + * @param ?callable $onCondition + * + * @return Rule + */ + protected function parseRestriction( + string $queryString, + string $restriction, + callable $onCondition = null + ): Filter\Rule { + $parser = QueryString::fromString($queryString); + if ($onCondition) { + $parser->on(QueryString::ON_CONDITION, $onCondition); + } + + return $parser->on( + QueryString::ON_CONDITION, + function (Filter\Condition $condition) use ($restriction, $queryString) { + $allowedColumns = ['report.name', 'report.author']; + if (in_array($condition->getColumn(), $allowedColumns, true)) { + return; + } + + throw new ConfigurationError( + t( + 'Cannot apply restriction %s using the filter %s.' + . ' You can only use the following columns: %s' + ), + $restriction, + $queryString, + implode(', ', $allowedColumns) + ); + } + )->parse(); + } +} diff --git a/library/Reporting/Web/Controller.php b/library/Reporting/Web/Controller.php index 1123332d..8446a98e 100644 --- a/library/Reporting/Web/Controller.php +++ b/library/Reporting/Web/Controller.php @@ -4,8 +4,10 @@ namespace Icinga\Module\Reporting\Web; +use Icinga\Module\Reporting\Common\Auth; use ipl\Web\Compat\CompatController; class Controller extends CompatController { + use Auth; } diff --git a/library/Reporting/Web/Forms/ReportForm.php b/library/Reporting/Web/Forms/ReportForm.php index 9b6e5c52..124c95c6 100644 --- a/library/Reporting/Web/Forms/ReportForm.php +++ b/library/Reporting/Web/Forms/ReportForm.php @@ -4,16 +4,20 @@ namespace Icinga\Module\Reporting\Web\Forms; -use Icinga\Authentication\Auth; +use Icinga\Authentication\Auth as IcingaAuth; +use Icinga\Module\Reporting\Common\Auth; use Icinga\Module\Reporting\Database; use Icinga\Module\Reporting\ProvidedReports; use ipl\Html\Contract\FormSubmitElement; use ipl\Html\Form; +use ipl\Stdlib\Filter; use ipl\Validator\CallbackValidator; use ipl\Web\Compat\CompatForm; +use ipl\Web\Filter\QueryString; class ReportForm extends CompatForm { + use Auth; use Database; use ProvidedReports; @@ -89,6 +93,42 @@ protected function assemble() return false; } + $report = (object) [ + 'report.name' => $value, + 'report.author' => IcingaAuth::getInstance()->getUser()->getUsername() + ]; + + $failedFilterRule = null; + $canCreate = true; + $restrictions = IcingaAuth::getInstance()->getRestrictions('reporting/reports'); + foreach ($restrictions as $restriction) { + $this->parseRestriction( + $restriction, + 'reporting/reports', + function (Filter\Condition $condition) use (&$canCreate, $report, &$failedFilterRule) { + if (! $canCreate || Filter::match($condition, $report)) { + return; + } + + $canCreate = false; + $failedFilterRule = QueryString::getRuleSymbol($condition) . $condition->getValue(); + } + ); + + if (! $canCreate) { + break; + } + } + + if (! $canCreate) { + $validator->addMessage(sprintf( + $this->translate('Please use report names that conform to this restriction: %s'), + 'name' . $failedFilterRule + )); + + return false; + } + return true; } ] @@ -171,7 +211,7 @@ public function onSuccess() if ($this->id === null) { $db->insert('report', [ 'name' => $values['name'], - 'author' => Auth::getInstance()->getUser()->getUsername(), + 'author' => IcingaAuth::getInstance()->getUser()->getUsername(), 'timeframe_id' => $values['timeframe'], 'template_id' => $values['template'], 'ctime' => $now,