Merge commit from fork

Check for permissions when evaluating object filters
This commit is contained in:
Julian Brost 2025-10-16 14:13:36 +02:00 committed by GitHub
commit 56255ac7a6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 647 additions and 42 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

@ -44,14 +44,24 @@ INITIALIZE_ONCE_WITH_PRIORITY([]() {
l_StatsNS->Freeze();
}, InitializePriority::FreezeNamespaces);
/**
* Construct a @c ScriptFrame that has `Self` assigned to the global namespace.
*
* Prefer the other constructor if possible since if misused this may leak global variables
* without permissions or senstive variables like TicketSalt in a sandboxed context.
*
* @todo Remove this constructor and call the other with the global namespace in places where it's actually necessary.
*/
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), Globals(nullptr)
{
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), Globals(nullptr)
{
InitializeFrame();
}
@ -63,6 +73,8 @@ void ScriptFrame::InitializeFrame()
if (frames && !frames->empty()) {
ScriptFrame *frame = frames->top();
// See the documentation of `ScriptFrame::Globals` for why these two are inherited and Globals isn't.
PermChecker = frame->PermChecker;
Sandboxed = frame->Sandboxed;
}
@ -79,6 +91,41 @@ ScriptFrame::~ScriptFrame()
#endif /* I2_DEBUG */
}
/**
* Returns a sanitized copy of the global variables namespace when sandboxed.
*
* This filters out the TicketSalt variable specifically and any variable for which the
* PermChecker does not return 'true'.
*
* However it specifically keeps the Types, System, and Icinga sub-namespaces, because they're
* accessed through globals in ScopeExpression and the user should have access to all Values
* contained in these namespaces.
*
* @return a sanitized copy of the global namespace if sandboxed, a pointer to the global namespace otherwise.
*/
Namespace::Ptr ScriptFrame::GetGlobals()
{
if (Sandboxed) {
if (!Globals) {
Globals = new Namespace;
auto globals = ScriptGlobal::GetGlobals();
ObjectLock lock{globals};
for (auto& [key, val] : globals) {
if (key == "TicketSalt") {
continue;
}
if (key == "Types" || key == "System" || key == "Icinga" || PermChecker->CanAccessGlobalVariable(key)) {
Globals->Set(key, val.Val, val.Const);
}
}
}
return Globals;
}
return ScriptGlobal::GetGlobals();
}
void ScriptFrame::IncreaseStackDepth()
{
if (Depth + 1 > 300)

View file

@ -5,18 +5,30 @@
#include "base/i2-base.hpp"
#include "base/dictionary.hpp"
#include "base/array.hpp"
#include "base/namespace.hpp"
#include "base/scriptpermission.hpp"
#include <boost/thread/tss.hpp>
#include <stack>
namespace icinga
{
/**
* A frame describing the context a section of script code is executed in.
*
* This is implemented by each new object that is constructed getting pushed on a thread_local
* global stack that is accessible from anywhere during script evaluation.
*
* Most properties in this frame, like local variables do not carry over to successive frames,
* except the `PermChecker` and `Sandboxed` members, which get propagated to enforce access
* control and availability of unsafe functions.
*/
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);
@ -28,7 +40,20 @@ struct ScriptFrame
static ScriptFrame *GetCurrentFrame();
Namespace::Ptr GetGlobals();
private:
/**
* Caches a sanitized version of the global namespace for the current `ScriptFrame`.
*
* This is a value that is dependent on a ScriptFrame's `Sandboxed` and `CheckPerms`
* members. These are both independent of each other and while they are inherited by
* subsequent frames themselves, their values can be changed for new frames easily.
* Therefore Globals can hold a different value for each ScriptFrame and is not
* inherited.
*/
Namespace::Ptr Globals;
static boost::thread_specific_ptr<std::stack<ScriptFrame *> > m_ScriptFrames;
static void PushFrame(ScriptFrame *frame);

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

@ -35,10 +35,10 @@ REGISTER_FUNCTION(System, exit, &Application::Exit, "status");
REGISTER_SAFE_FUNCTION(System, typeof, &ScriptUtils::TypeOf, "value");
REGISTER_SAFE_FUNCTION(System, keys, &ScriptUtils::Keys, "value");
REGISTER_SAFE_FUNCTION(System, random, &Utility::Random, "");
REGISTER_SAFE_FUNCTION(System, get_template, &ScriptUtils::GetTemplate, "type:name");
REGISTER_SAFE_FUNCTION(System, get_templates, &ScriptUtils::GetTemplates, "type");
REGISTER_FUNCTION(System, get_template, &ScriptUtils::GetTemplate, "type:name");
REGISTER_FUNCTION(System, get_templates, &ScriptUtils::GetTemplates, "type");
REGISTER_SAFE_FUNCTION(System, get_object, &ScriptUtils::GetObject, "type:name");
REGISTER_SAFE_FUNCTION(System, get_objects, &ScriptUtils::GetObjects, "type");
REGISTER_FUNCTION(System, get_objects, &ScriptUtils::GetObjects, "type");
REGISTER_FUNCTION(System, assert, &ScriptUtils::Assert, "value");
REGISTER_SAFE_FUNCTION(System, string, &ScriptUtils::CastString, "value");
REGISTER_SAFE_FUNCTION(System, number, &ScriptUtils::CastNumber, "value");
@ -46,7 +46,7 @@ REGISTER_SAFE_FUNCTION(System, bool, &ScriptUtils::CastBool, "value");
REGISTER_SAFE_FUNCTION(System, get_time, &Utility::GetTime, "");
REGISTER_SAFE_FUNCTION(System, basename, &Utility::BaseName, "path");
REGISTER_SAFE_FUNCTION(System, dirname, &Utility::DirName, "path");
REGISTER_SAFE_FUNCTION(System, getenv, &ScriptUtils::GetEnv, "value");
REGISTER_FUNCTION(System, getenv, &ScriptUtils::GetEnv, "value");
REGISTER_SAFE_FUNCTION(System, msi_get_component_path, &ScriptUtils::MsiGetComponentPathShim, "component");
REGISTER_SAFE_FUNCTION(System, escape_shell_cmd, &Utility::EscapeShellCmd, "cmd");
REGISTER_SAFE_FUNCTION(System, escape_shell_arg, &Utility::EscapeShellArg, "arg");
@ -473,7 +473,15 @@ ConfigObject::Ptr ScriptUtils::GetObject(const Value& vtype, const String& name)
if (!ctype)
return nullptr;
return ctype->GetObject(name);
auto cfgObj = ctype->GetObject(name);
if (cfgObj) {
auto* frame = ScriptFrame::GetCurrentFrame();
if (frame->PermChecker->CanAccessConfigObject(cfgObj)) {
return cfgObj;
}
}
return nullptr;
}
Array::Ptr ScriptUtils::GetObjects(const Type::Ptr& type)

View file

@ -118,8 +118,10 @@ ExpressionResult VariableExpression::DoEvaluate(ScriptFrame& frame, DebugHint *d
return value;
else if (VMOps::FindVarImport(frame, m_Imports, m_Variable, &value, m_DebugInfo))
return value;
else
return ScriptGlobal::Get(m_Variable);
else if (frame.GetGlobals()->Get(m_Variable, &value))
return value;
BOOST_THROW_EXCEPTION(ScriptError{"Tried to access undefined script variable '" + m_Variable + "'"});
}
bool VariableExpression::GetReference(ScriptFrame& frame, bool init_dict, Value *parent, String *index, DebugHint **dhint) const
@ -138,8 +140,8 @@ bool VariableExpression::GetReference(ScriptFrame& frame, bool init_dict, Value
*dhint = new DebugHint((*dhint)->GetChild(m_Variable));
} else if (VMOps::FindVarImportRef(frame, m_Imports, m_Variable, parent, m_DebugInfo)) {
return true;
} else if (ScriptGlobal::Exists(m_Variable)) {
*parent = ScriptGlobal::GetGlobals();
} else if (frame.GetGlobals()->Contains(m_Variable)) {
*parent = frame.GetGlobals();
if (dhint)
*dhint = nullptr;
@ -546,7 +548,7 @@ ExpressionResult GetScopeExpression::DoEvaluate(ScriptFrame& frame, DebugHint *d
else if (m_ScopeSpec == ScopeThis)
return frame.Self;
else if (m_ScopeSpec == ScopeGlobal)
return ScriptGlobal::GetGlobals();
return frame.GetGlobals();
else
VERIFY(!"Invalid scope.");
}

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,30 +19,32 @@ 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
{
{
Namespace::Ptr globals = ScriptGlobal::GetGlobals();
ObjectLock olock(globals);
for (const Namespace::Pair& kv : globals) {
addTarget(GetTargetForVar(kv.first, kv.second.Val));
Namespace::Ptr globals = ScriptGlobal::GetGlobals();
ObjectLock olock(globals);
for (auto& [key, value] : globals) {
/* We want wo avoid leaking the TicketSalt over the API, so we remove it here,
* as early as possible, so it isn't possible to abuse the fact that all of the
* global variables we return here later get checked against a user-provided
* filter expression that can cause its content to be printed in an error message
* or potentially access them otherwise.
*/
if (key == "TicketSalt") {
continue;
}
addTarget(FilterUtility::GetTargetForVar(key, value.Val));
}
}
Value GetTargetByName(const String& type, const String& name) const override
{
return GetTargetForVar(name, ScriptGlobal::Get(name));
if (name == "TicketSalt") {
BOOST_THROW_EXCEPTION(std::invalid_argument{"Access to TicketSalt via /v1/variables is not permitted."});
}
return FilterUtility::GetTargetForVar(name, ScriptGlobal::Get(name));
}
bool IsValidType(const String& type) const override
@ -99,9 +101,6 @@ bool VariableQueryHandler::HandleRequest(
ArrayData results;
for (Dictionary::Ptr var : objs) {
if (var->Get("name") == "TicketSalt")
continue;
results.emplace_back(new Dictionary({
{ "name", var->Get("name") },
{ "type", var->Get("type") },

View file

@ -119,6 +119,7 @@ set(base_test_SOURCES
icinga-perfdata.cpp
methods-pluginnotificationtask.cpp
remote-certificate-fixture.cpp
remote-filterutility.cpp
remote-configpackageutility.cpp
remote-httpserverconnection.cpp
remote-httpmessage.cpp

View file

@ -6,7 +6,8 @@
using namespace icinga;
BOOST_AUTO_TEST_SUITE(config_ops)
BOOST_AUTO_TEST_SUITE(config_ops,
*boost::unit_test::label("config"))
BOOST_AUTO_TEST_CASE(simple)
{
@ -243,4 +244,44 @@ BOOST_AUTO_TEST_CASE(advanced)
BOOST_CHECK(func->Invoke() == 3);
}
BOOST_AUTO_TEST_CASE(sandboxed_ticket_salt)
{
ScriptFrame frame(true, new Namespace);
std::unique_ptr<Expression> expr;
auto ns = ScriptGlobal::GetGlobals();
ns->Set("TicketSalt", "testvalue");
expr = ConfigCompiler::CompileText("<test>", "TicketSalt");
BOOST_CHECK_EQUAL(expr->Evaluate(frame).GetValue(), "testvalue");
expr = ConfigCompiler::CompileText("<test>", "globals.TicketSalt");
BOOST_CHECK_EQUAL(expr->Evaluate(frame).GetValue(), "testvalue");
expr = ConfigCompiler::CompileText("<test>", "*&TicketSalt");
BOOST_CHECK_EQUAL(expr->Evaluate(frame).GetValue(), "testvalue");
expr = ConfigCompiler::CompileText("<test>", "globals.TicketSalt = {{{other}}}");
BOOST_CHECK_NO_THROW(expr->Evaluate(frame));
frame.Sandboxed = true;
ns->Set("TicketSalt", "testvalue", false);
// Accessing TicketSalt in a sandboxed context is like trying to access a variable that doesn't exist.
// In case of direct access, it will throw a ScriptError.
expr = ConfigCompiler::CompileText("<test>", "TicketSalt");
BOOST_CHECK_THROW(expr->Evaluate(frame).GetValue(), ScriptError);
// In case of other ways of accessing it, like through the global scope, it evaluates to Empty
expr = ConfigCompiler::CompileText("<test>", "globals.TicketSalt");
BOOST_CHECK_EQUAL(expr->Evaluate(frame).GetValue(), "");
// Same for (the different ways of) trying to access it via a reference.
expr = ConfigCompiler::CompileText("<test>", "*&TicketSalt");
BOOST_CHECK_EQUAL(expr->Evaluate(frame).GetValue(), "");
expr = ConfigCompiler::CompileText("<test>", "globals.TicketSalt = {{{other}}}");
BOOST_CHECK_THROW(expr->Evaluate(frame), ScriptError);
}
BOOST_AUTO_TEST_SUITE_END()

View file

@ -0,0 +1,321 @@
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
#include <BoostTestTargetConfig.h>
#include "icinga/host.hpp"
#include "remote/apiuser.hpp"
#include "remote/filterutility.hpp"
#include "test/icingaapplication-fixture.hpp"
#include "config/configcompiler.hpp"
using namespace icinga;
// clang-format off
BOOST_AUTO_TEST_SUITE(remote_filterutility,
*boost::unit_test::label("config"))
// clang-format on
BOOST_FIXTURE_TEST_CASE(safe_function_permissions, IcingaApplicationFixture)
{
auto createObjects = []() {
String config = R"CONFIG({
object CheckCommand "dummy" {
command = "/bin/echo"
}
object ApiUser "allPermissionsUser" {
permissions = [ "*" ]
}
object ApiUser "permissionFilterUser" {
permissions = [
{
permission = "objects/query/Host"
filter = {{ host.name == {{{host1}}} }}
},
{
permission = "objects/query/Service"
filter = {{ service.name == {{{svc1}}} }}
}
]
}
object Host "host1" {
address = "host1"
check_command = "dummy"
}
object Host "host2" {
address = "host2"
check_command = "dummy"
}
object Service "svc1" {
host_name = "host1"
check_command = "dummy"
}
object Service "svc2" {
host_name = "host2"
check_command = "dummy"
}
})CONFIG";
std::unique_ptr<Expression> expr = ConfigCompiler::CompileText("<test>", config);
expr->Evaluate(*ScriptFrame::GetCurrentFrame());
};
ConfigItem::RunWithActivationContext(new Function("CreateTestObjects", createObjects));
auto allPermissionsUser = ApiUser::GetByName("allPermissionsUser");
auto permissionFilterUser = ApiUser::GetByName("permissionFilterUser");
QueryDescription qd;
qd.Types.insert("Host");
qd.Types.insert("Service");
qd.Permission = "objects/query/Host";
Dictionary::Ptr queryParams = new Dictionary();
queryParams->Set("type", "Host");
// This is a filter that uses a get_object call on an object the permissionFilterUser
// has access to. A second user is tested that has access to everything, to make sure
// the filter evaluates properly in the first place.
queryParams->Set("filter", "get_object(Host,{{{host1}}}).name == {{{host1}}}");
std::vector<Value> objs;
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, allPermissionsUser));
BOOST_CHECK_EQUAL(objs.size(), 2);
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, permissionFilterUser));
BOOST_CHECK_EQUAL(objs.size(), 1);
// We need to test again with querying services, while still using the get_object(Host) filter,
// because we need to verify permissions in filters work regardless of whether the object type
// that is queried is the same or different from the one that is checked in the filters.
qd.Permission = "objects/query/Service";
queryParams->Set("type", "Service");
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, allPermissionsUser));
BOOST_CHECK_EQUAL(objs.size(), 2);
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, permissionFilterUser));
BOOST_CHECK_EQUAL(objs.size(), 1);
// Now test again with a filter that always evaluates to false.
// Both users shouldn't find objects.
queryParams->Set("filter", "get_object(Host,{{{host2}}}).name == {{{host1}}}");
qd.Permission = "objects/query/Host";
queryParams->Set("type", "Host");
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, allPermissionsUser));
BOOST_CHECK(objs.empty());
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, permissionFilterUser));
BOOST_CHECK(objs.empty());
// Again, the same test with querying service objects instead of hosts.
qd.Permission = "objects/query/Service";
queryParams->Set("type", "Service");
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, allPermissionsUser));
BOOST_CHECK(objs.empty());
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, permissionFilterUser));
BOOST_CHECK(objs.empty());
// In the previous asserts we have established that filters work as intended with valid permissions.
// Now test again with a valid filter that tries to access a host object the permissionFilterUser
// doesn't have access to. It should still return an empty array.
queryParams->Set("filter", "get_object(Host,{{{host2}}}).name == {{{host2}}}");
qd.Permission = "objects/query/Host";
queryParams->Set("type", "Host");
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, allPermissionsUser));
BOOST_CHECK_EQUAL(objs.size(), 2);
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, permissionFilterUser));
BOOST_CHECK(objs.empty());
// Again, the same test with querying service objects instead of hosts.
qd.Permission = "objects/query/Service";
queryParams->Set("type", "Service");
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, allPermissionsUser));
BOOST_CHECK_EQUAL(objs.size(), 2);
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, permissionFilterUser));
BOOST_CHECK(objs.empty());
}
BOOST_FIXTURE_TEST_CASE(variable_expression_permissions, IcingaApplicationFixture)
{
auto createObjects = []() {
String config = R"CONFIG({
object CheckCommand "dummy" {
command = "/bin/echo"
}
object ApiUser "allPermissionsUser" {
permissions = [ "*" ]
}
object ApiUser "permissionFilterUser" {
permissions = [
"objects/query/Host",
{
permission = "variables"
filter = {{ variable.name != {{{SuperSecretConstant}}} }}
}
]
}
object ApiUser "noVariablePermUser" {
permissions = [ "objects/query/Host" ]
}
object Host "host1" {
address = "host1"
check_command = "dummy"
}
})CONFIG";
std::unique_ptr<Expression> expr = ConfigCompiler::CompileText("<test>", config);
expr->Evaluate(*ScriptFrame::GetCurrentFrame());
};
ConfigItem::RunWithActivationContext(new Function("CreateTestObjects", createObjects));
auto allPermissionsUser = ApiUser::GetByName("allPermissionsUser");
auto permissionFilterUser = ApiUser::GetByName("permissionFilterUser");
auto noVariablePermUser = ApiUser::GetByName("noVariablePermUser");
QueryDescription qd;
qd.Types.insert("Host");
qd.Types.insert("Service");
qd.Permission = "objects/query/Host";
ScriptGlobal::Set("VeryUsefulConstant", "Test1");
ScriptGlobal::Set("SuperSecretConstant", "MyCleartextBankingPassword");
ScriptGlobal::Set("TicketSalt", "Test2");
Dictionary::Ptr queryParams = new Dictionary();
queryParams->Set("type", "Host");
std::vector<Value> objs;
// First test a simple variable access.
// We expect the user with the right permissions to be able to query the object,
// while for a user without permission a ScriptError should be thrown.
queryParams->Set("filter", "VeryUsefulConstant == {{{Test1}}}");
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, allPermissionsUser));
BOOST_CHECK_EQUAL(objs.size(), 1);
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, permissionFilterUser));
BOOST_CHECK_EQUAL(objs.size(), 1);
BOOST_REQUIRE_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, noVariablePermUser), ScriptError);
// The variable can also be referenced and dereferenced.
// Unlike the variable access above indirectly accessing the variable without permissions does not
// throw a ScriptError but just returns an empty string.
queryParams->Set("filter", "*&VeryUsefulConstant == {{{Test1}}}");
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, allPermissionsUser));
BOOST_CHECK_EQUAL(objs.size(), 1);
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, permissionFilterUser));
BOOST_CHECK_EQUAL(objs.size(), 1);
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, noVariablePermUser));
BOOST_CHECK_EQUAL(objs.size(), 0);
// The variable can also be referenced and dereferenced via get(), which should have the
// same result as above.
queryParams->Set("filter", "(&VeryUsefulConstant).get() == {{{Test1}}}");
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, allPermissionsUser));
BOOST_CHECK_EQUAL(objs.size(), 1);
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, permissionFilterUser));
BOOST_CHECK_EQUAL(objs.size(), 1);
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, noVariablePermUser));
BOOST_CHECK_EQUAL(objs.size(), 0);
// Global variables can also be accessed via an IndexerExpression. The result should be the same as above.
queryParams->Set("filter", "globals[{{{VeryUsefulConstant}}}] == {{{Test1}}}");
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, allPermissionsUser));
BOOST_CHECK_EQUAL(objs.size(), 1);
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, permissionFilterUser));
BOOST_CHECK_EQUAL(objs.size(), 1);
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, noVariablePermUser));
BOOST_CHECK_EQUAL(objs.size(), 0);
// Now we verify that a user that isn't allowed to access a constant is not able to do so in a
// filter expression.
// The allPermissionsUser should be able to access the variable.
// The permissionFilterUser should receive an exception because they are specifically
// forbidden from reading that variable.
// Same for the noVariablePermUser, which as before isn't allowed to read any variable.
queryParams->Set("filter", "SuperSecretConstant == {{{MyCleartextBankingPassword}}}");
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, allPermissionsUser));
BOOST_CHECK_EQUAL(objs.size(), 1);
BOOST_REQUIRE_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, permissionFilterUser), ScriptError);
BOOST_REQUIRE_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, noVariablePermUser), ScriptError);
// Repeat the other ways to access secret variables, again, only the allPermissionsUser should
// be able to use it.
queryParams->Set("filter", "*&SuperSecretConstant == {{{MyCleartextBankingPassword}}}");
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, allPermissionsUser));
BOOST_CHECK_EQUAL(objs.size(), 1);
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, permissionFilterUser));
BOOST_CHECK_EQUAL(objs.size(), 0);
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, noVariablePermUser));
BOOST_CHECK_EQUAL(objs.size(), 0);
// Repeat the other ways to access secret variables, again, only the allPermissionsUser should
// be able to use it.
queryParams->Set("filter", "(&SuperSecretConstant).get() == {{{MyCleartextBankingPassword}}}");
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, allPermissionsUser));
BOOST_CHECK_EQUAL(objs.size(), 1);
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, permissionFilterUser));
BOOST_CHECK_EQUAL(objs.size(), 0);
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, noVariablePermUser));
BOOST_CHECK_EQUAL(objs.size(), 0);
// Repeat the other ways to access secret variables, again, only the allPermissionsUser should
// be able to use it.
queryParams->Set("filter", "globals[{{{SuperSecretConstant}}}] == {{{MyCleartextBankingPassword}}}");
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, allPermissionsUser));
BOOST_CHECK_EQUAL(objs.size(), 1);
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, permissionFilterUser));
BOOST_CHECK_EQUAL(objs.size(), 0);
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, noVariablePermUser));
BOOST_CHECK_EQUAL(objs.size(), 0);
// We also need to verify that even a user with all permissions can not access the TicketSalt variable.
// Like in the other cases above, direct access should throw.
queryParams->Set("filter", "TicketSalt == {{{Test2}}}");
BOOST_REQUIRE_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, allPermissionsUser), ScriptError);
BOOST_REQUIRE_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, permissionFilterUser), ScriptError);
BOOST_REQUIRE_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, noVariablePermUser), ScriptError);
// Repeat the other ways to access variables with TicketSalt.
queryParams->Set("filter", "*&TicketSalt == {{{Test2}}}");
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, allPermissionsUser));
BOOST_CHECK_EQUAL(objs.size(), 0);
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, permissionFilterUser));
BOOST_CHECK_EQUAL(objs.size(), 0);
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, noVariablePermUser));
BOOST_CHECK_EQUAL(objs.size(), 0);
// Repeat the other ways to access variables with TicketSalt.
queryParams->Set("filter", "(&TicketSalt).get() == {{{Test2}}}");
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, allPermissionsUser));
BOOST_CHECK_EQUAL(objs.size(), 0);
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, permissionFilterUser));
BOOST_CHECK_EQUAL(objs.size(), 0);
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, noVariablePermUser));
BOOST_CHECK_EQUAL(objs.size(), 0);
// Repeat the other ways to access variables with TicketSalt.
queryParams->Set("filter", "globals[{{{TicketSalt}}}] == {{{Test2}}}");
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, allPermissionsUser));
BOOST_CHECK_EQUAL(objs.size(), 0);
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, permissionFilterUser));
BOOST_CHECK_EQUAL(objs.size(), 0);
BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, noVariablePermUser));
BOOST_CHECK_EQUAL(objs.size(), 0);
}
BOOST_AUTO_TEST_SUITE_END()