diff --git a/lib/base/CMakeLists.txt b/lib/base/CMakeLists.txt index 1bb700c3d..f2eae0762 100644 --- a/lib/base/CMakeLists.txt +++ b/lib/base/CMakeLists.txt @@ -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 diff --git a/lib/base/scriptframe.cpp b/lib/base/scriptframe.cpp index 7a7f44c5f..f5e75731a 100644 --- a/lib/base/scriptframe.cpp +++ b/lib/base/scriptframe.cpp @@ -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; } diff --git a/lib/base/scriptframe.hpp b/lib/base/scriptframe.hpp index 18e23ef2f..93db5cb2b 100644 --- a/lib/base/scriptframe.hpp +++ b/lib/base/scriptframe.hpp @@ -5,7 +5,7 @@ #include "base/i2-base.hpp" #include "base/dictionary.hpp" -#include "base/array.hpp" +#include "base/scriptpermission.hpp" #include #include @@ -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); diff --git a/lib/base/scriptpermission.cpp b/lib/base/scriptpermission.cpp new file mode 100644 index 000000000..e985fa578 --- /dev/null +++ b/lib/base/scriptpermission.cpp @@ -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; +} diff --git a/lib/base/scriptpermission.hpp b/lib/base/scriptpermission.hpp new file mode 100644 index 000000000..ba6346cb3 --- /dev/null +++ b/lib/base/scriptpermission.hpp @@ -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 diff --git a/lib/remote/filterutility.cpp b/lib/remote/filterutility.cpp index 788a97b2e..cec2eb633 100644 --- a/lib/remote/filterutility.cpp +++ b/lib/remote/filterutility.cpp @@ -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>> m_PermCache; + ApiUser::Ptr m_User; +}; + Type::Ptr FilterUtility::TypeFromPluralName(const String& pluralName) { String uname = pluralName; @@ -211,8 +325,8 @@ std::vector FilterUtility::GetFilterTargets(const QueryDescription& qd, c else provider = new ConfigObjectTargetProvider(); - std::unique_ptr 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 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 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 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 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 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); }); } } diff --git a/lib/remote/filterutility.hpp b/lib/remote/filterutility.hpp index 7271367b4..73278d644 100644 --- a/lib/remote/filterutility.hpp +++ b/lib/remote/filterutility.hpp @@ -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* filter = nullptr); static bool HasPermission(const ApiUser::Ptr& user, const String& permission, std::unique_ptr* permissionFilter = nullptr); diff --git a/lib/remote/variablequeryhandler.cpp b/lib/remote/variablequeryhandler.cpp index e96f6abf8..7c38456a7 100644 --- a/lib/remote/variablequeryhandler.cpp +++ b/lib/remote/variablequeryhandler.cpp @@ -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& 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