Add permission checking to script frames and filter utilities

This commit is contained in:
Johannes Schmidt 2025-09-22 12:25:26 +02:00
parent 2063d2bdbc
commit 61670d5f23
8 changed files with 178 additions and 24 deletions

View File

@ -64,6 +64,7 @@ set(base_SOURCES
ringbuffer.cpp ringbuffer.hpp
scriptframe.cpp scriptframe.hpp
scriptglobal.cpp scriptglobal.hpp
scriptpermission.cpp scriptpermission.hpp
scriptutils.cpp scriptutils.hpp
serializer.cpp serializer.hpp
shared.hpp

View File

@ -45,13 +45,13 @@ INITIALIZE_ONCE_WITH_PRIORITY([]() {
}, InitializePriority::FreezeNamespaces);
ScriptFrame::ScriptFrame(bool allocLocals)
: Locals(allocLocals ? new Dictionary() : nullptr), Self(ScriptGlobal::GetGlobals()), Sandboxed(false), Depth(0)
: Locals(allocLocals ? new Dictionary() : nullptr), PermChecker(new ScriptPermissionChecker), Self(ScriptGlobal::GetGlobals()), Sandboxed(false), Depth(0)
{
InitializeFrame();
}
ScriptFrame::ScriptFrame(bool allocLocals, Value self)
: Locals(allocLocals ? new Dictionary() : nullptr), Self(std::move(self)), Sandboxed(false), Depth(0)
: Locals(allocLocals ? new Dictionary() : nullptr), PermChecker(new ScriptPermissionChecker), Self(std::move(self)), Sandboxed(false), Depth(0)
{
InitializeFrame();
}
@ -63,6 +63,7 @@ void ScriptFrame::InitializeFrame()
if (frames && !frames->empty()) {
ScriptFrame *frame = frames->top();
PermChecker = frame->PermChecker;
Sandboxed = frame->Sandboxed;
}

View File

@ -5,7 +5,7 @@
#include "base/i2-base.hpp"
#include "base/dictionary.hpp"
#include "base/array.hpp"
#include "base/scriptpermission.hpp"
#include <boost/thread/tss.hpp>
#include <stack>
@ -15,8 +15,9 @@ namespace icinga
struct ScriptFrame
{
Dictionary::Ptr Locals;
ScriptPermissionChecker::Ptr PermChecker; /* inherited by next frame */
Value Self;
bool Sandboxed;
bool Sandboxed; /* inherited by next frame */
int Depth;
ScriptFrame(bool allocLocals);

View File

@ -0,0 +1,15 @@
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
#include "base/scriptpermission.hpp"
using namespace icinga;
bool ScriptPermissionChecker::CanAccessGlobalVariable(const String&)
{
return true;
}
bool ScriptPermissionChecker::CanAccessConfigObject(const ConfigObject::Ptr&)
{
return true;
}

View File

@ -0,0 +1,28 @@
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
#pragma once
#include "base/string.hpp"
#include "base/shared-object.hpp"
#include "base/configobject.hpp"
namespace icinga {
class ScriptPermissionChecker : public SharedObject
{
public:
DECLARE_PTR_TYPEDEFS(ScriptPermissionChecker);
ScriptPermissionChecker() = default;
ScriptPermissionChecker(const ScriptPermissionChecker&) = delete;
ScriptPermissionChecker(ScriptPermissionChecker&&) = delete;
ScriptPermissionChecker& operator=(const ScriptPermissionChecker&) = delete;
ScriptPermissionChecker& operator=(ScriptPermissionChecker&&) = delete;
~ScriptPermissionChecker() override = default;
virtual bool CanAccessGlobalVariable(const String& varName);
virtual bool CanAccessConfigObject(const ConfigObject::Ptr& obj);
};
} // namespace icinga

View File

@ -15,6 +15,120 @@
using namespace icinga;
Dictionary::Ptr FilterUtility::GetTargetForVar(const String& name, const Value& value)
{
return new Dictionary({
{ "name", name },
{ "type", value.GetReflectionType()->GetName() },
{ "value", value }
});
}
/**
* Controls access to an object or variable based on an ApiUser's permissions.
*
* This is accomplished by caching the generated filter expressions so they don't have to be
* regenerated again and again when access is repeatedly checked in script functions and when
* evaluating expressions.
*/
class FilterExprPermissionChecker : public ScriptPermissionChecker
{
public:
DECLARE_PTR_TYPEDEFS(FilterExprPermissionChecker);
explicit FilterExprPermissionChecker(ApiUser::Ptr user) : m_User(std::move(user)) {}
/**
* Check if the user has the given permission and cache the result if they do.
*
* This is a wrapper around FilterUtility::CheckPermission() that caches the generated
* filter expression for later use when checking permissions inside sandboxed ScriptFrames.
*
* Like FilterUtility::CheckPermission() an exception is thrown if the user does not have
* the requested permission.
*
* If the user has permission and there is a filter for the given permission, the filter
* expression is generated, cached and then a pointer to it is returned, otherwise a
* nullptr will be returned.
*
* Since the optionally returned pointer is a raw-pointer and this class retains ownership
* over the expression it is only valid for the lifetime of the @c FilterExprPermissionChecker
* object that returned it.
*
* @param permissionString The permission string to check against the ApiUser member of this class.
*
* @return a pointer to the generated permission expression if the permission has a filter, or nullptr if not.
*/
Expression* CheckPermission(const String& permissionString)
{
auto [it, inserted] = m_PermCache.try_emplace(permissionString);
auto& [hasPermission, permissionExpr] = it->second;
if (inserted) {
FilterUtility::CheckPermission(m_User, permissionString, &permissionExpr);
} else if (!hasPermission) {
BOOST_THROW_EXCEPTION(ScriptError("Missing permission: " + permissionString.ToLower()));
}
hasPermission = true;
return permissionExpr.get();
}
/**
* Checks if this object's ApiUser has permissions to access variable `varName`.
*
* @param varName The name of the variable to check for access
*
* @return 'true' if the variable can be accessed, 'false' if it can't.
*/
bool CanAccessGlobalVariable(const String& varName) override
{
auto obj = FilterUtility::GetTargetForVar(varName, ScriptGlobal::Get(varName));
return CheckPermissionAndEvalFilter("variables", obj, "variable");
}
/**
* Checks if this object's ApiUser has permissions to access ConfigObject `obj`.
*
* @param obj A pointer to the ConfigObject to check for access
*
* @return 'true' if the object can be accessed, 'false' if it can't.
*/
bool CanAccessConfigObject(const ConfigObject::Ptr& obj) override
{
ASSERT(obj);
String perm = "objects/query/" + obj->GetReflectionType()->GetName();
String varName = obj->GetReflectionType()->GetName().ToLower();
return CheckPermissionAndEvalFilter(perm, obj, varName);
}
private:
bool CheckPermissionAndEvalFilter(const String& permissionString, const Object::Ptr& obj, const String& varName)
{
auto [it, inserted] = m_PermCache.try_emplace(permissionString);
auto& [hasPermission, permissionExpr] = it->second;
if (inserted) {
hasPermission = FilterUtility::HasPermission(m_User, permissionString, &permissionExpr);
}
if (hasPermission && permissionExpr) {
ScriptFrame permissionFrame(false, new Namespace());
// Sandboxing is lifted because this only evaluates the function from the
// ApiUser->permissions->filter
permissionFrame.Sandboxed = false;
return FilterUtility::EvaluateFilter(permissionFrame, permissionExpr.get(), obj, varName);
}
return hasPermission;
}
std::unordered_map<String, std::pair<bool, std::unique_ptr<Expression>>> m_PermCache;
ApiUser::Ptr m_User;
};
Type::Ptr FilterUtility::TypeFromPluralName(const String& pluralName)
{
String uname = pluralName;
@ -211,8 +325,8 @@ std::vector<Value> FilterUtility::GetFilterTargets(const QueryDescription& qd, c
else
provider = new ConfigObjectTargetProvider();
std::unique_ptr<Expression> permissionFilter;
CheckPermission(user, qd.Permission, &permissionFilter);
FilterExprPermissionChecker::Ptr permissionChecker = new FilterExprPermissionChecker{user};
auto* permissionFilter = permissionChecker->CheckPermission(qd.Permission);
Namespace::Ptr permissionFrameNS = new Namespace();
ScriptFrame permissionFrame(false, permissionFrameNS);
@ -228,7 +342,7 @@ std::vector<Value> FilterUtility::GetFilterTargets(const QueryDescription& qd, c
String name = HttpUtility::GetLastParameter(query, attr);
Object::Ptr target = provider->GetTargetByName(type, name);
if (!FilterUtility::EvaluateFilter(permissionFrame, permissionFilter.get(), target, variableName))
if (!FilterUtility::EvaluateFilter(permissionFrame, permissionFilter, target, variableName))
BOOST_THROW_EXCEPTION(ScriptError("Access denied to object '" + name + "' of type '" + type + "'"));
result.emplace_back(std::move(target));
@ -244,7 +358,7 @@ std::vector<Value> FilterUtility::GetFilterTargets(const QueryDescription& qd, c
for (String name : names) {
Object::Ptr target = provider->GetTargetByName(type, name);
if (!FilterUtility::EvaluateFilter(permissionFrame, permissionFilter.get(), target, variableName))
if (!FilterUtility::EvaluateFilter(permissionFrame, permissionFilter, target, variableName))
BOOST_THROW_EXCEPTION(ScriptError("Access denied to object '" + name + "' of type '" + type + "'"));
result.emplace_back(std::move(target));
@ -268,6 +382,7 @@ std::vector<Value> FilterUtility::GetFilterTargets(const QueryDescription& qd, c
Namespace::Ptr frameNS = new Namespace();
ScriptFrame frame(false, frameNS);
frame.Sandboxed = true;
frame.PermChecker = permissionChecker;
if (query->Contains("filter")) {
String filter = HttpUtility::GetLastParameter(query, "filter");
@ -322,7 +437,7 @@ std::vector<Value> FilterUtility::GetFilterTargets(const QueryDescription& qd, c
if (targeted) {
for (auto& target : targets) {
if (FilterUtility::EvaluateFilter(permissionFrame, permissionFilter.get(), target, variableName)) {
if (FilterUtility::EvaluateFilter(permissionFrame, permissionFilter, target, variableName)) {
result.emplace_back(std::move(target));
}
}
@ -335,16 +450,16 @@ std::vector<Value> FilterUtility::GetFilterTargets(const QueryDescription& qd, c
}
}
provider->FindTargets(type, [&permissionFrame, &permissionFilter, &frame, &ufilter, &result, variableName](const Object::Ptr& target) {
FilteredAddTarget(permissionFrame, permissionFilter.get(), frame, &*ufilter, result, variableName, target);
provider->FindTargets(type, [&permissionFrame, permissionFilter, &frame, &ufilter, &result, variableName](const Object::Ptr& target) {
FilteredAddTarget(permissionFrame, permissionFilter, frame, &*ufilter, result, variableName, target);
});
}
} else {
/* Ensure to pass a nullptr as filter expression.
* GCC 8.1.1 on F28 causes problems, see GH #6533.
*/
provider->FindTargets(type, [&permissionFrame, &permissionFilter, &frame, &result, variableName](const Object::Ptr& target) {
FilteredAddTarget(permissionFrame, permissionFilter.get(), frame, nullptr, result, variableName, target);
provider->FindTargets(type, [&permissionFrame, permissionFilter, &frame, &result, variableName](const Object::Ptr& target) {
FilteredAddTarget(permissionFrame, permissionFilter, frame, nullptr, result, variableName, target);
});
}
}

View File

@ -50,6 +50,8 @@ struct QueryDescription
class FilterUtility
{
public:
static Dictionary::Ptr GetTargetForVar(const String& name, const Value& value);
static Type::Ptr TypeFromPluralName(const String& pluralName);
static void CheckPermission(const ApiUser::Ptr& user, const String& permission, std::unique_ptr<Expression>* filter = nullptr);
static bool HasPermission(const ApiUser::Ptr& user, const String& permission, std::unique_ptr<Expression>* permissionFilter = nullptr);

View File

@ -19,15 +19,6 @@ class VariableTargetProvider final : public TargetProvider
public:
DECLARE_PTR_TYPEDEFS(VariableTargetProvider);
static Dictionary::Ptr GetTargetForVar(const String& name, const Value& value)
{
return new Dictionary({
{ "name", name },
{ "type", value.GetReflectionType()->GetName() },
{ "value", value }
});
}
void FindTargets(const String& type,
const std::function<void (const Value&)>& addTarget) const override
{
@ -35,14 +26,14 @@ public:
Namespace::Ptr globals = ScriptGlobal::GetGlobals();
ObjectLock olock(globals);
for (const Namespace::Pair& kv : globals) {
addTarget(GetTargetForVar(kv.first, kv.second.Val));
addTarget(FilterUtility::GetTargetForVar(kv.first, kv.second.Val));
}
}
}
Value GetTargetByName(const String& type, const String& name) const override
{
return GetTargetForVar(name, ScriptGlobal::Get(name));
return FilterUtility::GetTargetForVar(name, ScriptGlobal::Get(name));
}
bool IsValidType(const String& type) const override