monitoring: Apply custom variable restrictions

refs #10965
This commit is contained in:
Alexander A. Klimov 2016-03-23 13:00:53 +01:00 committed by Eric Lippmann
parent 66a7bdfc84
commit 589da9bcd1
3 changed files with 514 additions and 62 deletions

View File

@ -0,0 +1,180 @@
<?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] = '**';
} else {
$pattern[$i] = '/^' . $subPattern . '$/';
}
}
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

@ -12,6 +12,7 @@ use Icinga\Data\Filterable;
use Icinga\Exception\InvalidPropertyException; use Icinga\Exception\InvalidPropertyException;
use Icinga\Exception\ProgrammingError; use Icinga\Exception\ProgrammingError;
use Icinga\Module\Monitoring\Backend\MonitoringBackend; use Icinga\Module\Monitoring\Backend\MonitoringBackend;
use Icinga\Util\GlobFilter;
use Icinga\Web\UrlParams; use Icinga\Web\UrlParams;
/** /**
@ -151,7 +152,7 @@ abstract class MonitoredObject implements Filterable
/** /**
* The properties to hide from the user * The properties to hide from the user
* *
* @var array * @var GlobFilter
*/ */
protected $blacklistedProperties = null; protected $blacklistedProperties = null;
@ -503,69 +504,15 @@ abstract class MonitoredObject implements Filterable
protected function hideBlacklistedProperties() protected function hideBlacklistedProperties()
{ {
if ($this->blacklistedProperties === null) { if ($this->blacklistedProperties === null) {
$this->blacklistedProperties = array(); $this->blacklistedProperties = new GlobFilter(
foreach (Auth::getInstance()->getRestrictions('monitoring/blacklist/properties') as $patterns) { Auth::getInstance()->getRestrictions('monitoring/blacklist/properties')
foreach (explode(',', $patterns) as $pattern) { );
$pattern = explode('.', $pattern);
foreach ($pattern as & $subPattern) {
$subPattern = explode('*', $subPattern);
foreach ($subPattern as & $subPatternPart) {
if ($subPatternPart !== '') {
$subPatternPart = preg_quote($subPatternPart, '/');
}
unset($subPatternPart);
}
$subPattern = '/^' . implode('.*', $subPattern) . '$/';
unset($subPattern);
}
$this->blacklistedProperties[] = $pattern;
}
}
} }
$allProperties = array($this->type => array('vars' => $this->customvars)); $allProperties = $this->blacklistedProperties->removeMatching(
foreach ($this->blacklistedProperties as $blacklistedProperty) { array($this->type => array('vars' => $this->customvars))
$allProperties = $this->hideBlacklistedPropertiesRecursive($allProperties, $blacklistedProperty); );
} $this->customvars = isset($allProperties[$this->type]['vars']) ? $allProperties[$this->type]['vars'] : array();
$this->customvars = $allProperties[$this->type]['vars'];
}
/**
* Helper method for hideBlacklistedProperties()
*
* @param stdClass|array $allProperties
* @param array $blacklistedProperty
*
* @return stdClass|array
*/
protected function hideBlacklistedPropertiesRecursive($allProperties, $blacklistedProperty)
{
$isObject = $allProperties instanceof stdClass;
if ($isObject || is_array($allProperties)) {
if ($isObject) {
$allProperties = (array) $allProperties;
}
$currentLevel = $blacklistedProperty[0];
$nextLevels = count($blacklistedProperty) === 1 ? null : array_slice($blacklistedProperty, 1);
foreach ($allProperties as $k => & $v) {
if (preg_match($currentLevel, (string) $k)) {
if ($nextLevels === null) {
unset($allProperties[$k]);
} else {
$v = $this->hideBlacklistedPropertiesRecursive($v, $nextLevels);
}
}
unset($v);
}
if ($isObject) {
$allProperties = (object) $allProperties;
}
}
return $allProperties;
} }
/** /**

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' => ''
)
)
)
)
)
);
}
}