icingadb-web/library/Icingadb/Authentication/ObjectAuthorization.php

269 lines
8.6 KiB
PHP
Raw Permalink Normal View History

2021-01-22 09:24:27 -05:00
<?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;
2024-11-12 11:53:17 -05:00
use Icinga\Module\Icingadb\Model\DependencyNode;
2021-01-22 09:24:27 -05:00
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 = [];
2021-01-22 09:24:27 -05:00
/**
* 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
2021-01-22 09:24:27 -05:00
{
$self = new static();
$tableName = $for->getTableName();
$uniqueId = $for->{$for->getKeyName()};
if (! isset($uniqueId)) {
return false;
}
if (! isset(self::$knownGrants[$tableName][$uniqueId])) {
$self->loadGrants(
2021-01-22 09:24:27 -05:00
get_class($for),
Filter::equal($for->getKeyName(), $uniqueId),
$uniqueId,
false
2021-01-22 09:24:27 -05:00
);
}
return $self->checkGrants($permission, self::$knownGrants[$tableName][$uniqueId]);
2021-01-22 09:24:27 -05:00
}
/**
* 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
2021-01-22 09:24:27 -05:00
* 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.
2021-01-22 09:24:27 -05:00
*
* @param string $permission
* @param string $type
* @param Filter\Rule $filter
* @param bool $cache Pass `false` to not perform the check on every object
2021-01-22 09:24:27 -05:00
*
* @return bool
*/
public static function grantsOnType(string $permission, string $type, Filter\Rule $filter, bool $cache = true): bool
2021-01-22 09:24:27 -05:00
{
switch ($type) {
case 'host':
$for = Host::class;
break;
case 'service':
$for = Service::class;
break;
2024-11-12 11:53:17 -05:00
case 'dependency_node':
$for = DependencyNode::class;
break;
2021-01-22 09:24:27 -05:00
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);
2021-01-22 09:24:27 -05:00
}
return $self->checkGrants($permission, self::$knownGrants[$type][$uniqueId]);
2021-01-22 09:24:27 -05:00
}
/**
* 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];
}
2021-01-22 09:24:27 -05:00
/**
* 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
2021-01-22 09:24:27 -05:00
*
* @return void
2021-01-22 09:24:27 -05:00
*/
protected function loadGrants(string $model, Filter\Rule $filter, string $cacheKey, bool $cache = true)
2021-01-22 09:24:27 -05:00
{
/** @var Model $model */
$query = $model::on($this->getDb());
$tableName = $query->getModel()->getTableName();
2021-01-22 09:24:27 -05:00
$inspectedRoles = [];
$roleExpressions = [];
$rolesWithoutRestrictions = [];
foreach ($this->getAuth()->getUser()->getRoles() as $role) {
2021-01-22 09:24:27 -05:00
$roleFilter = Filter::all();
if (($restriction = $role->getRestrictions('icingadb/filter/objects'))) {
$roleFilter->add($this->parseRestriction($restriction, 'icingadb/filter/objects'));
}
2024-11-12 11:53:17 -05:00
if ($tableName === 'host' || $tableName === 'service' || $tableName === 'dependency_node') {
2021-01-22 09:24:27 -05:00
if (($restriction = $role->getRestrictions('icingadb/filter/hosts'))) {
$roleFilter->add($this->parseRestriction($restriction, 'icingadb/filter/hosts'));
}
}
2024-11-12 11:53:17 -05:00
if (
($tableName === 'dependency_node' || $tableName === 'service')
&& ($restriction = $role->getRestrictions('icingadb/filter/services'))
) {
2021-01-22 09:24:27 -05:00
$roleFilter->add($this->parseRestriction($restriction, 'icingadb/filter/services'));
}
if ($roleFilter->isEmpty()) {
$rolesWithoutRestrictions[] = $role->getName();
2021-01-22 09:24:27 -05:00
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();
}
2021-01-22 09:24:27 -05:00
}
$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;
2021-01-22 09:24:27 -05:00
}
}
}
self::$knownGrants[$tableName][$cacheKey] = array_merge(
$rolesWithoutRestrictions,
array_keys($rolesWithRestrictions)
);
2021-01-22 09:24:27 -05:00
}
/**
* 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
2021-01-22 09:24:27 -05:00
{
if (empty($roles)) {
return false;
}
$granted = false;
2021-01-22 09:24:27 -05:00
foreach ($this->getAuth()->getUser()->getRoles() as $role) {
if ($role->denies($permission)) {
return false;
} elseif ($granted || ! $role->grants($permission)) {
2021-01-22 09:24:27 -05:00
continue;
}
$granted = in_array($role->getName(), $roles, true);
2021-01-22 09:24:27 -05:00
}
return $granted;
2021-01-22 09:24:27 -05:00
}
}