From e6eb716bbb2ae24e81e1e08eb0188275e455b211 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Mon, 10 Jul 2023 17:21:00 +0200 Subject: [PATCH] Allow restricting reports by `name` & `author` --- application/controllers/ReportController.php | 6 +- application/controllers/ReportsController.php | 10 ++- configuration.php | 5 ++ doc/03-Configuration.md | 13 ++++ library/Reporting/Common/Auth.php | 73 +++++++++++++++++++ library/Reporting/Web/Controller.php | 2 + library/Reporting/Web/Forms/ReportForm.php | 40 +++++++++- 7 files changed, 141 insertions(+), 8 deletions(-) create mode 100644 library/Reporting/Common/Auth.php 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..777d3416 100644 --- a/application/controllers/ReportsController.php +++ b/application/controllers/ReportsController.php @@ -43,6 +43,8 @@ public function indexAction() $reports = Report::on($this->getDb()) ->withColumns(['report.timeframe.name']); + $this->applyRestrictions($reports); + $sortControl = $this->createSortControl( $reports, [ @@ -64,16 +66,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..88d2a41b 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,38 @@ protected function assemble() return false; } + $report = (object) [ + 'report.name' => $value, + 'report.author' => IcingaAuth::getInstance()->getUser()->getUsername() + ]; + + $onCondition = function (Filter\Condition $condition) use (&$filterNames): void { + if ($condition->getColumn() == 'report.name') { + $filterNames[] = QueryString::getRuleSymbol($condition) . $condition->getValue(); + } + }; + + $restrictions = IcingaAuth::getInstance()->getRestrictions('reporting/reports'); + $matched = false; + $filterNames = []; + foreach ($restrictions as $restriction) { + $filter = $this->parseRestriction($restriction, 'reporting/reports', $onCondition); + if (Filter::match($filter, $report)) { + $matched = true; + break; + } + } + + if (! empty($restrictions) && ! $matched) { + $validator->addMessage(sprintf( + $this->translate('Please use report names that conform to this restriction: %s%s'), + 'name', + implode(',', $filterNames) + )); + + return false; + } + return true; } ] @@ -171,7 +207,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,