Skip to content

Commit bc1d083

Browse files
committed
Allow restricting reports by name & author
1 parent 971c7e9 commit bc1d083

File tree

7 files changed

+185
-17
lines changed

7 files changed

+185
-17
lines changed

application/controllers/ReportController.php

+4-2
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,11 @@ public function init()
3535

3636
$report = Model\Report::on($this->getDb())
3737
->with(['timeframe'])
38-
->filter(Filter::equal('id', $reportId))
39-
->first();
38+
->filter(Filter::equal('id', $reportId));
4039

40+
$this->applyRestrictions($report);
41+
42+
$report = $report->first();
4143
if ($report === null) {
4244
$this->httpNotFound($this->translate('Report not found'));
4345
}

application/controllers/ReportsController.php

+46-13
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Icinga\Module\Reporting\Controllers;
66

7+
use Icinga\Authentication\Auth as IcingaAuth;
78
use Icinga\Module\Icingadb\ProvidedHook\Reporting\HostSlaReport;
89
use Icinga\Module\Icingadb\ProvidedHook\Reporting\ServiceSlaReport;
910
use Icinga\Module\Reporting\Database;
@@ -12,6 +13,7 @@
1213
use Icinga\Module\Reporting\Web\Forms\ReportForm;
1314
use Icinga\Module\Reporting\Web\ReportsTimeframesAndTemplatesTabs;
1415
use ipl\Html\Html;
16+
use ipl\Stdlib\Filter;
1517
use ipl\Web\Url;
1618
use ipl\Web\Widget\ButtonLink;
1719
use ipl\Web\Widget\Icon;
@@ -27,22 +29,53 @@ public function indexAction()
2729
$this->createTabs()->activate('reports');
2830

2931
if ($this->hasPermission('reporting/reports')) {
30-
$this->addControl(new ButtonLink(
31-
$this->translate('New Report'),
32-
Url::fromPath('reporting/reports/new'),
33-
'plus',
34-
[
35-
'data-icinga-modal' => true,
36-
'data-no-icinga-ajax' => true
37-
]
38-
));
32+
$canCreate = true;
33+
$report = ['report.author' => $this->auth->getUser()->getUsername()];
34+
$restrictions = IcingaAuth::getInstance()->getRestrictions('reporting/reports');
35+
foreach ($restrictions as $restriction) {
36+
$this->parseRestriction(
37+
$restriction,
38+
'reporting/reports',
39+
function (Filter\Condition $condition) use (&$canCreate, $report) {
40+
if ($condition->getColumn() != 'report.author') {
41+
// Only filters like `report.author!=$user.local_name$` can fully prevent the current user
42+
// from creating his own reports.
43+
return;
44+
}
45+
46+
if (! $canCreate || Filter::match($condition, $report)) {
47+
return;
48+
}
49+
50+
$canCreate = false;
51+
}
52+
);
53+
54+
if (! $canCreate) {
55+
break;
56+
}
57+
}
58+
59+
if ($canCreate) {
60+
$this->addControl(new ButtonLink(
61+
$this->translate('New Report'),
62+
Url::fromPath('reporting/reports/new'),
63+
'plus',
64+
[
65+
'data-icinga-modal' => true,
66+
'data-no-icinga-ajax' => true
67+
]
68+
));
69+
}
3970
}
4071

4172
$tableRows = [];
4273

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

77+
$this->applyRestrictions($reports);
78+
4679
$sortControl = $this->createSortControl(
4780
$reports,
4881
[
@@ -64,16 +97,16 @@ public function indexAction()
6497
Html::tag('td', null, $report->timeframe->name),
6598
Html::tag('td', null, $report->ctime->format('Y-m-d H:i')),
6699
Html::tag('td', null, $report->mtime->format('Y-m-d H:i')),
67-
Html::tag('td', ['class' => 'icon-col'], [
68-
new Link(
100+
! $this->hasPermission('reporting/reports')
101+
? null
102+
: Html::tag('td', ['class' => 'icon-col'], new Link(
69103
new Icon('edit'),
70104
Url::fromPath('reporting/report/edit', ['id' => $report->id]),
71105
[
72106
'data-icinga-modal' => true,
73107
'data-no-icinga-ajax' => true
74108
]
75-
)
76-
])
109+
))
77110
]);
78111
}
79112

configuration.php

+5
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,9 @@
5050
'reporting/timeframes',
5151
$this->translate('Allow managing timeframes')
5252
);
53+
54+
$this->provideRestriction(
55+
'reporting/reports',
56+
$this->translate('Restrict access to the reports that match the provided filter')
57+
);
5358
}

doc/03-Configuration.md

+13
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,16 @@ reporting/reports | Reports (create, edit, delete)
3131
reporting/schedules | Schedules (create, edit, delete)
3232
reporting/templates | Templates (create, edit, delete)
3333
reporting/timeframes | Timeframes (create, edit, delete)
34+
35+
## Restrictions
36+
37+
Icinga Reporting currently provides a single restriction that can be used to limit users to a specific set of reports,
38+
while having the `reporting/reports` permission.
39+
40+
> **Note:**
41+
>
42+
> Filters from multiple roles will expand the available access.
43+
44+
| Name | Description |
45+
|-------------------|---------------------------------------------------------------|
46+
| reporting/reports | Restrict access to the reports that match the provided filter |

library/Reporting/Common/Auth.php

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
/* Icinga Reporting | (c) 2023 Icinga GmbH | GPLv2 */
4+
5+
namespace Icinga\Module\Reporting\Common;
6+
7+
use Icinga\Authentication\Auth as IcingaAuth;
8+
use Icinga\Exception\ConfigurationError;
9+
use ipl\Orm\Query;
10+
use ipl\Stdlib\Filter;
11+
use ipl\Stdlib\Filter\Rule;
12+
use ipl\Web\Filter\QueryString;
13+
14+
trait Auth
15+
{
16+
/**
17+
* Apply restrictions of this module
18+
*
19+
* @param Query $query
20+
*/
21+
protected function applyRestrictions(Query $query): void
22+
{
23+
$auth = IcingaAuth::getInstance();
24+
$restrictions = $auth->getRestrictions('reporting/reports');
25+
26+
$queryFilter = Filter::any();
27+
foreach ($restrictions as $restriction) {
28+
$queryFilter->add($this->parseRestriction($restriction, 'reporting/reports'));
29+
}
30+
31+
$query->filter($queryFilter);
32+
}
33+
34+
/**
35+
* Parse the query string of the given restriction
36+
*
37+
* @param string $queryString
38+
* @param string $restriction
39+
* @param ?callable $onCondition
40+
*
41+
* @return Rule
42+
*/
43+
protected function parseRestriction(
44+
string $queryString,
45+
string $restriction,
46+
callable $onCondition = null
47+
): Filter\Rule {
48+
$parser = QueryString::fromString($queryString);
49+
if ($onCondition) {
50+
$parser->on(QueryString::ON_CONDITION, $onCondition);
51+
}
52+
53+
return $parser->on(
54+
QueryString::ON_CONDITION,
55+
function (Filter\Condition $condition) use ($restriction, $queryString) {
56+
$allowedColumns = ['report.name', 'report.author'];
57+
if (in_array($condition->getColumn(), $allowedColumns, true)) {
58+
return;
59+
}
60+
61+
throw new ConfigurationError(
62+
t(
63+
'Cannot apply restriction %s using the filter %s.'
64+
. ' You can only use the following columns: %s'
65+
),
66+
$restriction,
67+
$queryString,
68+
implode(', ', $allowedColumns)
69+
);
70+
}
71+
)->parse();
72+
}
73+
}

library/Reporting/Web/Controller.php

+2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
namespace Icinga\Module\Reporting\Web;
66

7+
use Icinga\Module\Reporting\Common\Auth;
78
use ipl\Web\Compat\CompatController;
89

910
class Controller extends CompatController
1011
{
12+
use Auth;
1113
}

library/Reporting/Web/Forms/ReportForm.php

+42-2
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,20 @@
44

55
namespace Icinga\Module\Reporting\Web\Forms;
66

7-
use Icinga\Authentication\Auth;
7+
use Icinga\Authentication\Auth as IcingaAuth;
8+
use Icinga\Module\Reporting\Common\Auth;
89
use Icinga\Module\Reporting\Database;
910
use Icinga\Module\Reporting\ProvidedReports;
1011
use ipl\Html\Contract\FormSubmitElement;
1112
use ipl\Html\Form;
13+
use ipl\Stdlib\Filter;
1214
use ipl\Validator\CallbackValidator;
1315
use ipl\Web\Compat\CompatForm;
16+
use ipl\Web\Filter\QueryString;
1417

1518
class ReportForm extends CompatForm
1619
{
20+
use Auth;
1721
use Database;
1822
use ProvidedReports;
1923

@@ -89,6 +93,42 @@ protected function assemble()
8993
return false;
9094
}
9195

96+
$report = (object) [
97+
'report.name' => $value,
98+
'report.author' => IcingaAuth::getInstance()->getUser()->getUsername()
99+
];
100+
101+
$failedFilterRule = null;
102+
$canCreate = true;
103+
$restrictions = IcingaAuth::getInstance()->getRestrictions('reporting/reports');
104+
foreach ($restrictions as $restriction) {
105+
$this->parseRestriction(
106+
$restriction,
107+
'reporting/reports',
108+
function (Filter\Condition $condition) use (&$canCreate, $report, &$failedFilterRule) {
109+
if (! $canCreate || Filter::match($condition, $report)) {
110+
return;
111+
}
112+
113+
$canCreate = false;
114+
$failedFilterRule = QueryString::getRuleSymbol($condition) . $condition->getValue();
115+
}
116+
);
117+
118+
if (! $canCreate) {
119+
break;
120+
}
121+
}
122+
123+
if (! $canCreate) {
124+
$validator->addMessage(sprintf(
125+
$this->translate('Please use report names that conform to this restriction: %s'),
126+
'name' . $failedFilterRule
127+
));
128+
129+
return false;
130+
}
131+
92132
return true;
93133
}
94134
]
@@ -171,7 +211,7 @@ public function onSuccess()
171211
if ($this->id === null) {
172212
$db->insert('report', [
173213
'name' => $values['name'],
174-
'author' => Auth::getInstance()->getUser()->getUsername(),
214+
'author' => IcingaAuth::getInstance()->getUser()->getUsername(),
175215
'timeframe_id' => $values['timeframe'],
176216
'template_id' => $values['template'],
177217
'ctime' => $now,

0 commit comments

Comments
 (0)