diff --git a/library/Icinga/Util/GlobFilter.php b/library/Icinga/Util/GlobFilter.php new file mode 100644 index 000000000..f0dde9f97 --- /dev/null +++ b/library/Icinga/Util/GlobFilter.php @@ -0,0 +1,180 @@ + 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; + } +} diff --git a/modules/monitoring/library/Monitoring/Object/MonitoredObject.php b/modules/monitoring/library/Monitoring/Object/MonitoredObject.php index 1f1ba7375..437268eb8 100644 --- a/modules/monitoring/library/Monitoring/Object/MonitoredObject.php +++ b/modules/monitoring/library/Monitoring/Object/MonitoredObject.php @@ -12,6 +12,7 @@ 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; /** @@ -151,7 +152,7 @@ abstract class MonitoredObject implements Filterable /** * The properties to hide from the user * - * @var array + * @var GlobFilter */ protected $blacklistedProperties = null; @@ -503,69 +504,15 @@ abstract class MonitoredObject implements Filterable protected function hideBlacklistedProperties() { if ($this->blacklistedProperties === null) { - $this->blacklistedProperties = array(); - foreach (Auth::getInstance()->getRestrictions('monitoring/blacklist/properties') as $patterns) { - 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; - } - } + $this->blacklistedProperties = new GlobFilter( + Auth::getInstance()->getRestrictions('monitoring/blacklist/properties') + ); } - $allProperties = array($this->type => array('vars' => $this->customvars)); - foreach ($this->blacklistedProperties as $blacklistedProperty) { - $allProperties = $this->hideBlacklistedPropertiesRecursive($allProperties, $blacklistedProperty); - } - $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; + $allProperties = $this->blacklistedProperties->removeMatching( + array($this->type => array('vars' => $this->customvars)) + ); + $this->customvars = isset($allProperties[$this->type]['vars']) ? $allProperties[$this->type]['vars'] : array(); } /** diff --git a/test/php/library/Icinga/Util/GlobFilterTest.php b/test/php/library/Icinga/Util/GlobFilterTest.php new file mode 100644 index 000000000..2ec0e101f --- /dev/null +++ b/test/php/library/Icinga/Util/GlobFilterTest.php @@ -0,0 +1,325 @@ +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' => '' + ) + ) + ) + ) + ) + ); + } +}