Merge branch 'feature/restrict-custom-variables-10965'

resolves #10965
This commit is contained in:
Eric Lippmann 2016-04-13 15:55:26 +02:00
commit 118055c07d
5 changed files with 619 additions and 1 deletions

View File

@ -0,0 +1,182 @@
<?php
/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
namespace Icinga\Util;
use stdClass;
/**
* GLOB-like filter for simple data structures
*
* e.g. this filters:
*
* foo.bar.baz
* foo.b*r.baz
* **.baz
*
* match this one:
*
* array(
* 'foo' => array(
* 'bar' => array(
* 'baz' => 'deadbeef' // <---
* )
* )
* )
*/
class GlobFilter
{
/**
* The prepared filters
*
* @var array
*/
protected $filters;
/**
* Create a new filter from a comma-separated list of GLOB-like filters or an array of such lists.
*
* @param string|\Traversable $filters
*/
public function __construct($filters)
{
$patterns = array(array(''));
$lastIndex1 = $lastIndex2 = 0;
foreach ((is_string($filters) ? array($filters) : $filters) as $rawPatterns) {
$escape = false;
foreach (str_split($rawPatterns) as $c) {
if ($escape) {
$escape = false;
$patterns[$lastIndex1][$lastIndex2] .= preg_quote($c, '/');
} else {
switch ($c) {
case '\\':
$escape = true;
break;
case ',':
$patterns[] = array('');
++$lastIndex1;
$lastIndex2 = 0;
break;
case '.':
$patterns[$lastIndex1][] = '';
++$lastIndex2;
break;
case '*':
$patterns[$lastIndex1][$lastIndex2] .= '.*';
break;
default:
$patterns[$lastIndex1][$lastIndex2] .= preg_quote($c, '/');
}
}
}
if ($escape) {
$patterns[$lastIndex1][$lastIndex2] .= '\\\\';
}
}
$this->filters = array();
foreach ($patterns as $pattern) {
foreach ($pattern as $i => $subPattern) {
if ($subPattern === '') {
unset($pattern[$i]);
} elseif ($subPattern === '.*.*') {
$pattern[$i] = '**';
} elseif ($subPattern === '.*') {
$pattern[$i] = '/^' . $subPattern . '$/';
} else {
$pattern[$i] = '/^' . trim($subPattern) . '$/i';
}
}
if (! empty($pattern)) {
$found = false;
foreach ($pattern as $i => $v) {
if ($found) {
if ($v === '**') {
unset($pattern[$i]);
} else {
$found = false;
}
} elseif ($v === '**') {
$found = true;
}
}
if (end($pattern) === '**') {
$pattern[] = '/^.*$/';
}
$this->filters[] = array_values($pattern);
}
}
}
/**
* Remove all keys/attributes matching any of $this->filters from $dataStructure
*
* @param stdClass|array $dataStructure
*
* @return stdClass|array The modified copy of $dataStructure
*/
public function removeMatching($dataStructure)
{
foreach ($this->filters as $filter) {
$dataStructure = static::removeMatchingRecursive($dataStructure, $filter);
}
return $dataStructure;
}
/**
* Helper method for removeMatching()
*
* @param stdClass|array $dataStructure
* @param array $filter
*
* @return stdClass|array
*/
protected static function removeMatchingRecursive($dataStructure, $filter)
{
$multiLevelPattern = $filter[0] === '**';
if ($multiLevelPattern) {
$dataStructure = static::removeMatchingRecursive($dataStructure, array_slice($filter, 1));
}
$isObject = $dataStructure instanceof stdClass;
if ($isObject || is_array($dataStructure)) {
if ($isObject) {
$dataStructure = (array) $dataStructure;
}
if ($multiLevelPattern) {
foreach ($dataStructure as $k => & $v) {
$v = static::removeMatchingRecursive($v, $filter);
unset($v);
}
} else {
$currentLevel = $filter[0];
$nextLevels = count($filter) === 1 ? null : array_slice($filter, 1);
foreach ($dataStructure as $k => & $v) {
if (preg_match($currentLevel, (string) $k)) {
if ($nextLevels === null) {
unset($dataStructure[$k]);
} else {
$v = static::removeMatchingRecursive($v, $nextLevels);
}
}
unset($v);
}
}
if ($isObject) {
$dataStructure = (object) $dataStructure;
}
}
return $dataStructure;
}
}

View File

@ -84,6 +84,10 @@ $this->provideRestriction(
'monitoring/filter/objects',
$this->translate('Restrict views to the Icinga objects that match the filter')
);
$this->provideRestriction(
'monitoring/blacklist/properties',
$this->translate('Hide the properties of monitored objects that match the filter')
);
$this->provideConfigTab('backends', array(
'title' => $this->translate('Configure how to retrieve monitoring information'),

View File

@ -0,0 +1,77 @@
# Restrict Access to Custom Variables (WIP)
* Restriction name: monitoring/blacklist/properties
* Restriction value: Comma separated list of GLOB like filters
Imagine the following host custom variable structure.
```
host.vars.
|-- cmdb_name
|-- cmdb_id
|-- cmdb_location
|-- wiki_id
|-- passwords.
| |-- mysql_password
| |-- ldap_password
| `-- mongodb_password
|-- legacy.
| |-- cmdb_name
| |-- mysql_password
| `-- wiki_id
`-- backup.
`-- passwords.
|-- mysql_password
`-- ldap_password
```
`host.vars.cmdb_name`
Blacklists cmdb_name in the first level of the custom variable structure only.
`host.vars.legacy.cmdb_name` is not blacklisted.
`host.vars.cmdb_*`
All custom variables in the first level of the structure which begin with `cmdb_` become blacklisted.
Deeper custom variables are ignored. `host.vars.legacy.cmdb_name` is not blacklisted.
`host.vars.*id`
All custom variables in the first level of the structure which end with `id` become blacklisted.
Deeper custom variables are ignored. `host.vars.legacy.wiki_id` is not blacklisted.
`host.vars.*.mysql_password`
Matches all custom variables on the second level which are equal to `mysql_password`.
`host.vars.*.*password`
Matches all custom variables on the second level which end with `password`.
`host.vars.*.mysql_password,host.vars.*.ldap_password`
Matches all custorm variables on the second level which equal `mysql_password` or `ldap_password`.
`host.vars.**.*password`
Matches all custom variables on all levels which end with `password`.
Please note the two asterisks, `**`, here for crossing level boundaries. This syntax is used for matching the complete
custom variable structure.
If you want to restrict all custom variables that end with password for both hosts and services, you have to define
the following restriction.
`host.vars.**.*password,service.vars.**.*password`
## Escape Meta Characters
Use backslash to escape the meta characters
* *
* ,
`host.vars.\*fall`
Matches all custom variables in the first level which equal `*fall`.

View File

@ -5,12 +5,14 @@ namespace Icinga\Module\Monitoring\Object;
use stdClass;
use InvalidArgumentException;
use Icinga\Authentication\Auth;
use Icinga\Application\Config;
use Icinga\Data\Filter\Filter;
use Icinga\Data\Filterable;
use Icinga\Exception\InvalidPropertyException;
use Icinga\Exception\ProgrammingError;
use Icinga\Module\Monitoring\Backend\MonitoringBackend;
use Icinga\Util\GlobFilter;
use Icinga\Web\UrlParams;
/**
@ -147,6 +149,13 @@ abstract class MonitoredObject implements Filterable
*/
protected $stats;
/**
* The properties to hide from the user
*
* @var GlobFilter
*/
protected $blacklistedProperties = null;
/**
* Create a monitored object, i.e. host or service
*
@ -457,7 +466,9 @@ abstract class MonitoredObject implements Filterable
$customvars = $this->hostVariables;
}
$this->customvars = $this->obfuscateCustomVars($customvars, $blacklistPattern);
$this->customvars = $customvars;
$this->hideBlacklistedProperties();
$this->customvars = $this->obfuscateCustomVars($this->customvars, $blacklistPattern);
return $this;
}
@ -485,6 +496,25 @@ abstract class MonitoredObject implements Filterable
return $customvars instanceof stdClass ? (object) $obfuscatedCustomVars : $obfuscatedCustomVars;
}
/**
* Hide all blacklisted properties from the user as restricted by monitoring/blacklist/properties
*
* Currently this only affects the custom variables
*/
protected function hideBlacklistedProperties()
{
if ($this->blacklistedProperties === null) {
$this->blacklistedProperties = new GlobFilter(
Auth::getInstance()->getRestrictions('monitoring/blacklist/properties')
);
}
$allProperties = $this->blacklistedProperties->removeMatching(
array($this->type => array('vars' => $this->customvars))
);
$this->customvars = isset($allProperties[$this->type]['vars']) ? $allProperties[$this->type]['vars'] : array();
}
/**
* Fetch the host custom variables related to this object
*

View File

@ -0,0 +1,325 @@
<?php
/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
namespace Tests\Icinga\Util;
use Icinga\Util\GlobFilter;
use Icinga\Test\BaseTestCase;
class GlobFilterTest extends BaseTestCase
{
protected function assertGlobFilterRemovesMatching($filterPattern, $unfiltered, $filtered)
{
$filter = new GlobFilter($filterPattern);
$this->assertTrue(
$filter->removeMatching($unfiltered) === $filtered,
'Filter `' . $filterPattern . '\' doesn\'t work as intended'
);
}
public function testPatternWithoutAnyWildcards()
{
$this->assertGlobFilterRemovesMatching(
'host.vars.cmdb_name',
array(
'host' => array(
'vars' => array(
'cmdb_name' => '',
'cmdb_id' => '',
'legacy' => array(
'cmdb_name' => ''
)
)
)
),
array(
'host' => array(
'vars' => array(
'cmdb_id' => '',
'legacy' => array(
'cmdb_name' => ''
)
)
)
)
);
}
public function testPatternWithAnAsteriskAtTheEndOfAComponent()
{
$this->assertGlobFilterRemovesMatching(
'host.vars.cmdb_*',
array(
'host' => array(
'vars' => array(
'cmdb_name' => '',
'cmdb_id' => '',
'cmdb_location' => '',
'wiki_id' => '',
'legacy' => array(
'cmdb_name' => ''
)
)
)
),
array(
'host' => array(
'vars' => array(
'wiki_id' => '',
'legacy' => array(
'cmdb_name' => ''
)
)
)
)
);
}
public function testPatternWithAnAsteriskAtTheBeginningOfAComponent()
{
$this->assertGlobFilterRemovesMatching(
'host.vars.*id',
array(
'host' => array(
'vars' => array(
'cmdb_name' => '',
'cmdb_id' => '',
'wiki_id' => '',
'legacy' => array(
'wiki_id' => ''
)
)
)
),
array(
'host' => array(
'vars' => array(
'cmdb_name' => '',
'legacy' => array(
'wiki_id' => ''
)
)
)
)
);
}
public function testPatternWithAComponentBeingTheAsteriskOnly()
{
$this->assertGlobFilterRemovesMatching(
'host.vars.*.mysql_password',
array(
'host' => array(
'vars' => array(
'cmdb_name' => '',
'passwords' => array(
'mysql_password' => '',
'ldap_password' => ''
),
'legacy' => array(
'mysql_password' => ''
),
'backup' => array(
'passwords' => array(
'mysql_password' => ''
)
)
)
)
),
array(
'host' => array(
'vars' => array(
'cmdb_name' => '',
'passwords' => array(
'ldap_password' => ''
),
'legacy' => array(),
'backup' => array(
'passwords' => array(
'mysql_password' => ''
)
)
)
)
)
);
}
public function testPatternWithTwoComponentsContainingAsterisks()
{
$this->assertGlobFilterRemovesMatching(
'host.vars.*.*password',
array(
'host' => array(
'vars' => array(
'cmdb_name' => '',
'passwords' => array(
'mysql_password' => '',
'ldap_password' => '',
'mongodb_password' => ''
),
'legacy' => array(
'cmdb_name' => '',
'mysql_password' => ''
),
'backup' => array(
'passwords' => array(
'mysql_password' => '',
'ldap_password' => ''
)
)
)
)
),
array(
'host' => array(
'vars' => array(
'cmdb_name' => '',
'passwords' => array(),
'legacy' => array(
'cmdb_name' => ''
),
'backup' => array(
'passwords' => array(
'mysql_password' => '',
'ldap_password' => ''
)
)
)
)
)
);
}
public function testTwoCommaSeparatedPatternsEachWithAnAsterisk()
{
$this->assertGlobFilterRemovesMatching(
'host.vars.*.mysql_password,host.vars.*.ldap_password',
array(
'host' => array(
'vars' => array(
'cmdb_name' => '',
'passwords' => array(
'mysql_password' => '',
'ldap_password' => '',
'mongodb_password' => ''
),
'legacy' => array(
'mysql_password' => ''
),
'backup' => array(
'passwords' => array(
'mysql_password' => '',
'ldap_password' => ''
)
)
)
)
),
array(
'host' => array(
'vars' => array(
'cmdb_name' => '',
'passwords' => array(
'mongodb_password' => ''
),
'legacy' => array(),
'backup' => array(
'passwords' => array(
'mysql_password' => '',
'ldap_password' => ''
)
)
)
)
)
);
}
public function testPatternWithAComponentBeingTheMultiLevelWildcard()
{
$this->assertGlobFilterRemovesMatching(
'host.vars.**.*password',
array(
'host' => array(
'vars' => array(
'cmdb_location' => '',
'passwords' => array(
'mysql_password' => '',
'ldap_password' => '',
'mongodb_password' => ''
),
'legacy' => array(
'mysql_password' => '',
),
'backup' => array(
'passwords' => array(
'mysql_password' => '',
'ldap_password' => ''
)
)
)
)
),
array(
'host' => array(
'vars' => array(
'cmdb_location' => '',
'passwords' => array(),
'legacy' => array(),
'backup' => array(
'passwords' => array()
)
)
)
)
);
}
public function testPatternWithAnEscapedAsterisk()
{
$this->assertGlobFilterRemovesMatching(
'host.vars.**.\*password',
array(
'host' => array(
'vars' => array(
'wiki_id' => '',
'passwords' => array(
'mongodb_password' => '',
'*password' => ''
),
'legacy' => array(
'mysql_password' => '',
'*password' => ''
),
'backup' => array(
'passwords' => array(
'*password' => '',
'ldap_password' => ''
)
)
)
)
),
array(
'host' => array(
'vars' => array(
'wiki_id' => '',
'passwords' => array(
'mongodb_password' => ''
),
'legacy' => array(
'mysql_password' => ''
),
'backup' => array(
'passwords' => array(
'ldap_password' => ''
)
)
)
)
)
);
}
}