icinga2/test/remote-filterutility.cpp
2025-10-02 15:51:42 +02:00

322 lines
14 KiB
C++

/* 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()