mirror of
https://github.com/Icinga/icinga2.git
synced 2025-10-25 17:24:10 +02:00
Add permission checking to script frames and filter utilities
This commit is contained in:
parent
2063d2bdbc
commit
61670d5f23
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
15
lib/base/scriptpermission.cpp
Normal file
15
lib/base/scriptpermission.cpp
Normal 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;
|
||||
}
|
||||
28
lib/base/scriptpermission.hpp
Normal file
28
lib/base/scriptpermission.hpp
Normal 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
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user