Skip to content

Commit 1508211

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

File tree

7 files changed

+139
-8
lines changed

7 files changed

+139
-8
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

+6-4
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ public function indexAction()
4343
$reports = Report::on($this->getDb())
4444
->withColumns(['report.timeframe.name']);
4545

46+
$this->applyRestrictions($reports);
47+
4648
$sortControl = $this->createSortControl(
4749
$reports,
4850
[
@@ -64,16 +66,16 @@ public function indexAction()
6466
Html::tag('td', null, $report->timeframe->name),
6567
Html::tag('td', null, $report->ctime->format('Y-m-d H:i')),
6668
Html::tag('td', null, $report->mtime->format('Y-m-d H:i')),
67-
Html::tag('td', ['class' => 'icon-col'], [
68-
new Link(
69+
! $this->hasPermission('reporting/reports')
70+
? null
71+
: Html::tag('td', ['class' => 'icon-col'], new Link(
6972
new Icon('edit'),
7073
Url::fromPath('reporting/report/edit', ['id' => $report->id]),
7174
[
7275
'data-icinga-modal' => true,
7376
'data-no-icinga-ajax' => true
7477
]
75-
)
76-
])
78+
))
7779
]);
7880
}
7981

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

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

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

+38-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,38 @@ 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+
$onCondition = function (Filter\Condition $condition) use (&$filterNames): void {
102+
if ($condition->getColumn() == 'report.name') {
103+
$filterNames[] = QueryString::getRuleSymbol($condition) . $condition->getValue();
104+
}
105+
};
106+
107+
$restrictions = IcingaAuth::getInstance()->getRestrictions('reporting/reports');
108+
$matched = false;
109+
$filterNames = [];
110+
foreach ($restrictions as $restriction) {
111+
$filter = $this->parseRestriction($restriction, 'reporting/reports', $onCondition);
112+
if (Filter::match($filter, $report)) {
113+
$matched = true;
114+
break;
115+
}
116+
}
117+
118+
if (! empty($restrictions) && ! $matched) {
119+
$validator->addMessage(sprintf(
120+
$this->translate('Please use report names that conform to this restriction: %s%s'),
121+
'name',
122+
implode(',', $filterNames)
123+
));
124+
125+
return false;
126+
}
127+
92128
return true;
93129
}
94130
]
@@ -171,7 +207,7 @@ public function onSuccess()
171207
if ($this->id === null) {
172208
$db->insert('report', [
173209
'name' => $values['name'],
174-
'author' => Auth::getInstance()->getUser()->getUsername(),
210+
'author' => IcingaAuth::getInstance()->getUser()->getUsername(),
175211
'timeframe_id' => $values['timeframe'],
176212
'template_id' => $values['template'],
177213
'ctime' => $now,

0 commit comments

Comments
 (0)