Skip to content

Commit

Permalink
Allow restricting reports by name & author
Browse files Browse the repository at this point in the history
  • Loading branch information
yhabteab committed Jul 13, 2023
1 parent 971c7e9 commit a0423b5
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 17 deletions.
6 changes: 4 additions & 2 deletions application/controllers/ReportController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
}
Expand Down
60 changes: 47 additions & 13 deletions application/controllers/ReportsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -12,6 +13,8 @@
use Icinga\Module\Reporting\Web\Forms\ReportForm;
use Icinga\Module\Reporting\Web\ReportsTimeframesAndTemplatesTabs;
use ipl\Html\Html;
use ipl\Stdlib\Filter;
use ipl\Web\Filter\QueryString;
use ipl\Web\Url;
use ipl\Web\Widget\ButtonLink;
use ipl\Web\Widget\Icon;
Expand All @@ -27,22 +30,53 @@ 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 = [];

$reports = Report::on($this->getDb())
->withColumns(['report.timeframe.name']);

$this->applyRestrictions($reports);

$sortControl = $this->createSortControl(
$reports,
[
Expand All @@ -64,16 +98,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
]
)
])
))
]);
}

Expand Down
5 changes: 5 additions & 0 deletions configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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')
);
}
13 changes: 13 additions & 0 deletions doc/03-Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
73 changes: 73 additions & 0 deletions library/Reporting/Common/Auth.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

/* Icinga Reporting | (c) 2023 Icinga GmbH | GPLv2 */

namespace Icinga\Module\Reporting\Common;

use Icinga\Authentication\Auth as IcingaAuth;
use Icinga\Exception\ConfigurationError;
use ipl\Orm\Query;
use ipl\Stdlib\Filter;
use ipl\Stdlib\Filter\Rule;
use ipl\Web\Filter\QueryString;

trait Auth
{
/**
* Apply restrictions of this module
*
* @param Query $query
*/
protected function applyRestrictions(Query $query): void
{
$auth = IcingaAuth::getInstance();
$restrictions = $auth->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();
}
}
2 changes: 2 additions & 0 deletions library/Reporting/Web/Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
44 changes: 42 additions & 2 deletions library/Reporting/Web/Forms/ReportForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}
]
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit a0423b5

Please sign in to comment.