summaryrefslogtreecommitdiffstats
path: root/library/Icingadb/Authentication/ObjectAuthorization.php
diff options
context:
space:
mode:
Diffstat (limited to 'library/Icingadb/Authentication/ObjectAuthorization.php')
-rw-r--r--library/Icingadb/Authentication/ObjectAuthorization.php261
1 files changed, 261 insertions, 0 deletions
diff --git a/library/Icingadb/Authentication/ObjectAuthorization.php b/library/Icingadb/Authentication/ObjectAuthorization.php
new file mode 100644
index 0000000..988e8f0
--- /dev/null
+++ b/library/Icingadb/Authentication/ObjectAuthorization.php
@@ -0,0 +1,261 @@
+<?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\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;
+ 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') {
+ if (($restriction = $role->getRestrictions('icingadb/filter/hosts'))) {
+ $roleFilter->add($this->parseRestriction($restriction, 'icingadb/filter/hosts'));
+ }
+ }
+
+ if ($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;
+ }
+}