icingadb-web/library/Icingadb/Authentication/ObjectAuthorization.php
2024-11-19 17:44:53 +01:00

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;
}
}