Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow restricting reports by name & author #189

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
59 changes: 46 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,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;
Expand All @@ -27,22 +29,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 +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
]
)
])
))
]);
}

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'];
lippserd marked this conversation as resolved.
Show resolved Hide resolved
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