mirror of
https://github.com/Icinga/icingadb-web.git
synced 2026-03-04 22:30:29 -05:00
268 lines
8.6 KiB
PHP
268 lines
8.6 KiB
PHP
<?php
|
|
|
|
/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
|
|
|
|
namespace Icinga\Module\Icingadb\Authentication;
|
|
|
|
use Icinga\Module\Icingadb\Common\Auth;
|
|
use Icinga\Module\Icingadb\Common\Database;
|
|
use Icinga\Module\Icingadb\Model\DependencyNode;
|
|
use Icinga\Module\Icingadb\Model\Host;
|
|
use Icinga\Module\Icingadb\Model\Service;
|
|
use InvalidArgumentException;
|
|
use ipl\Orm\Compat\FilterProcessor;
|
|
use ipl\Orm\Model;
|
|
use ipl\Sql\Expression;
|
|
use ipl\Sql\Select;
|
|
use ipl\Stdlib\Filter;
|
|
|
|
class ObjectAuthorization
|
|
{
|
|
use Auth;
|
|
use Database;
|
|
|
|
/** @var array */
|
|
protected static $knownGrants = [];
|
|
|
|
/**
|
|
* Caches already applied filters to an object
|
|
*
|
|
* @var array
|
|
*/
|
|
protected static $matchedFilters = [];
|
|
|
|
/**
|
|
* Check whether the permission is granted on the object
|
|
*
|
|
* @param string $permission
|
|
* @param Model $for The object
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function grantsOn(string $permission, Model $for): bool
|
|
{
|
|
$self = new static();
|
|
|
|
$tableName = $for->getTableName();
|
|
$uniqueId = $for->{$for->getKeyName()};
|
|
if (! isset($uniqueId)) {
|
|
return false;
|
|
}
|
|
|
|
if (! isset(self::$knownGrants[$tableName][$uniqueId])) {
|
|
$self->loadGrants(
|
|
get_class($for),
|
|
Filter::equal($for->getKeyName(), $uniqueId),
|
|
$uniqueId,
|
|
false
|
|
);
|
|
}
|
|
|
|
return $self->checkGrants($permission, self::$knownGrants[$tableName][$uniqueId]);
|
|
}
|
|
|
|
/**
|
|
* Check whether the permission is granted on objects matching the type and filter
|
|
*
|
|
* The check will be performed on every object matching the filter. Though the result
|
|
* only allows to determine whether the permission is granted on **any** or *none*
|
|
* of the objects in question. Any subsequent call to {@see ObjectAuthorization::grantsOn}
|
|
* will make use of the underlying results the check has determined in order to avoid
|
|
* unnecessary queries.
|
|
*
|
|
* @param string $permission
|
|
* @param string $type
|
|
* @param Filter\Rule $filter
|
|
* @param bool $cache Pass `false` to not perform the check on every object
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function grantsOnType(string $permission, string $type, Filter\Rule $filter, bool $cache = true): bool
|
|
{
|
|
switch ($type) {
|
|
case 'host':
|
|
$for = Host::class;
|
|
break;
|
|
case 'service':
|
|
$for = Service::class;
|
|
break;
|
|
case 'dependency_node':
|
|
$for = DependencyNode::class;
|
|
break;
|
|
default:
|
|
throw new InvalidArgumentException(sprintf('Unknown type "%s"', $type));
|
|
}
|
|
|
|
$self = new static();
|
|
|
|
$uniqueId = spl_object_hash($filter);
|
|
if (! isset(self::$knownGrants[$type][$uniqueId])) {
|
|
$self->loadGrants($for, $filter, $uniqueId, $cache);
|
|
}
|
|
|
|
return $self->checkGrants($permission, self::$knownGrants[$type][$uniqueId]);
|
|
}
|
|
|
|
/**
|
|
* Check whether the given filter matches on the given object
|
|
*
|
|
* @param string $queryString
|
|
* @param Model $object
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function matchesOn(string $queryString, Model $object): bool
|
|
{
|
|
$self = new static();
|
|
|
|
$uniqueId = $object->{$object->getKeyName()};
|
|
if (! isset(self::$matchedFilters[$queryString][$uniqueId])) {
|
|
$restriction = 'icingadb/filter/services';
|
|
if ($object instanceof Host) {
|
|
$restriction = 'icingadb/filter/hosts';
|
|
}
|
|
|
|
$filter = $self->parseRestriction($queryString, $restriction);
|
|
|
|
$query = $object::on($self->getDb());
|
|
$query
|
|
->filter($filter)
|
|
->filter(Filter::equal($object->getKeyName(), $uniqueId))
|
|
->columns([new Expression('1')]);
|
|
|
|
$result = $query->execute()->hasResult();
|
|
self::$matchedFilters[$queryString][$uniqueId] = $result;
|
|
|
|
return $result;
|
|
}
|
|
|
|
return self::$matchedFilters[$queryString][$uniqueId];
|
|
}
|
|
|
|
/**
|
|
* Load all the user's roles that grant access to at least one object matching the filter
|
|
*
|
|
* @param string $model The class path to the object model
|
|
* @param Filter\Rule $filter
|
|
* @param string $cacheKey
|
|
* @param bool $cache Pass `false` to not populate the cache with the matching objects
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function loadGrants(string $model, Filter\Rule $filter, string $cacheKey, bool $cache = true)
|
|
{
|
|
/** @var Model $model */
|
|
$query = $model::on($this->getDb());
|
|
$tableName = $query->getModel()->getTableName();
|
|
|
|
$inspectedRoles = [];
|
|
$roleExpressions = [];
|
|
$rolesWithoutRestrictions = [];
|
|
|
|
foreach ($this->getAuth()->getUser()->getRoles() as $role) {
|
|
$roleFilter = Filter::all();
|
|
if (($restriction = $role->getRestrictions('icingadb/filter/objects'))) {
|
|
$roleFilter->add($this->parseRestriction($restriction, 'icingadb/filter/objects'));
|
|
}
|
|
|
|
if ($tableName === 'host' || $tableName === 'service' || $tableName === 'dependency_node') {
|
|
if (($restriction = $role->getRestrictions('icingadb/filter/hosts'))) {
|
|
$roleFilter->add($this->parseRestriction($restriction, 'icingadb/filter/hosts'));
|
|
}
|
|
}
|
|
|
|
if (
|
|
($tableName === 'dependency_node' || $tableName === 'service')
|
|
&& ($restriction = $role->getRestrictions('icingadb/filter/services'))
|
|
) {
|
|
$roleFilter->add($this->parseRestriction($restriction, 'icingadb/filter/services'));
|
|
}
|
|
|
|
if ($roleFilter->isEmpty()) {
|
|
$rolesWithoutRestrictions[] = $role->getName();
|
|
continue;
|
|
}
|
|
|
|
$roleName = str_replace('.', '_', $role->getName());
|
|
$inspectedRoles[$roleName] = $role->getName();
|
|
|
|
$roleName = $this->getDb()->quoteIdentifier($roleName);
|
|
|
|
if ($cache) {
|
|
FilterProcessor::apply($roleFilter, $query);
|
|
$where = $query->getSelectBase()->getWhere();
|
|
$query->getSelectBase()->resetWhere();
|
|
|
|
$values = [];
|
|
$rendered = $this->getDb()->getQueryBuilder()->buildCondition($where, $values);
|
|
$roleExpressions[$roleName] = new Expression($rendered, null, ...$values);
|
|
} else {
|
|
$subQuery = clone $query;
|
|
$roleExpressions[$roleName] = $subQuery
|
|
->columns([new Expression('1')])
|
|
->filter($roleFilter)
|
|
->filter($filter)
|
|
->limit(1)
|
|
->assembleSelect()
|
|
->resetOrderBy();
|
|
}
|
|
}
|
|
|
|
$rolesWithRestrictions = [];
|
|
if (! empty($roleExpressions)) {
|
|
if ($cache) {
|
|
$query->columns('id')->withColumns($roleExpressions);
|
|
$query->filter($filter);
|
|
} else {
|
|
$query = [$this->getDb()->fetchOne((new Select())->columns($roleExpressions))];
|
|
}
|
|
|
|
foreach ($query as $row) {
|
|
$roles = $rolesWithoutRestrictions;
|
|
foreach ($inspectedRoles as $alias => $roleName) {
|
|
if ($row->$alias) {
|
|
$rolesWithRestrictions[$roleName] = true;
|
|
$roles[] = $roleName;
|
|
}
|
|
}
|
|
|
|
if ($cache) {
|
|
self::$knownGrants[$tableName][$row->id] = $roles;
|
|
}
|
|
}
|
|
}
|
|
|
|
self::$knownGrants[$tableName][$cacheKey] = array_merge(
|
|
$rolesWithoutRestrictions,
|
|
array_keys($rolesWithRestrictions)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if any of the given roles grants the permission
|
|
*
|
|
* @param string $permission
|
|
* @param array $roles
|
|
*
|
|
* @return bool
|
|
*/
|
|
protected function checkGrants(string $permission, array $roles): bool
|
|
{
|
|
if (empty($roles)) {
|
|
return false;
|
|
}
|
|
|
|
$granted = false;
|
|
foreach ($this->getAuth()->getUser()->getRoles() as $role) {
|
|
if ($role->denies($permission)) {
|
|
return false;
|
|
} elseif ($granted || ! $role->grants($permission)) {
|
|
continue;
|
|
}
|
|
|
|
$granted = in_array($role->getName(), $roles, true);
|
|
}
|
|
|
|
return $granted;
|
|
}
|
|
}
|