diff --git a/doc/02-installation.md b/doc/02-installation.md
index d369f5111..dbfc38a2b 100644
--- a/doc/02-installation.md
+++ b/doc/02-installation.md
@@ -363,6 +363,15 @@ Run the following command to:
icinga2 api setup
```
+For new installations, it is recommended to set the following additional attribute inside the `ApiListener` object
+definition in `/etc/icinga2/features-enabled/api.conf`. This will already enforce stricter permissions as they will
+become the default with v2.17 (see the [upgrading documentation](16-upgrading-icinga-2.md#upgrading-to-2-16-2) for the
+version that introduced that setting for more details):
+
+```
+enforce_filter_expression_permission = true
+```
+
Restart Icinga 2 for these changes to take effect.
```bash
diff --git a/doc/09-object-types.md b/doc/09-object-types.md
index ca45194ac..37a28902a 100644
--- a/doc/09-object-types.md
+++ b/doc/09-object-types.md
@@ -1088,28 +1088,29 @@ object ApiListener "api" {
Configuration Attributes:
- Name | Type | Description
- --------------------------------------|-----------------------|----------------------------------
- cert\_path | String | **Deprecated.** Path to the public key.
- key\_path | String | **Deprecated.** Path to the private key.
- ca\_path | String | **Deprecated.** Path to the CA certificate file.
- ticket\_salt | String | **Optional.** Private key for [CSR auto-signing](06-distributed-monitoring.md#distributed-monitoring-setup-csr-auto-signing). **Required** for a signing master instance.
- crl\_path | String | **Optional.** Path to the CRL file.
- bind\_host | String | **Optional.** The IP address the api listener should be bound to. If not specified, the ApiListener is bound to `::` and listens for both IPv4 and IPv6 connections or to `0.0.0.0` if IPv6 is not supported by the operating system.
- bind\_port | Number | **Optional.** The port the api listener should be bound to. Defaults to `5665`.
- accept\_config | Boolean | **Optional.** Accept zone configuration. Defaults to `false`.
- accept\_commands | Boolean | **Optional.** Accept remote commands. Defaults to `false`.
- max\_anonymous\_clients | Number | **Optional.** Limit the number of anonymous client connections (not configured endpoints and signing requests).
- cipher\_list | String | **Optional.** Cipher list that is allowed. For a list of available ciphers run `openssl ciphers`. Defaults to `ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256`.
- tls\_protocolmin | String | **Optional.** Minimum TLS protocol version. Since v2.11, only `TLSv1.2` is supported. Defaults to `TLSv1.2`.
- tls\_handshake\_timeout | Number | **Deprecated.** TLS Handshake timeout. Defaults to `10s`.
- connect\_timeout | Number | **Optional.** Timeout for establishing new connections. Affects both incoming and outgoing connections. Within this time, the TCP and TLS handshakes must complete and either a HTTP request or an Icinga cluster connection must be initiated. Defaults to `15s`.
- access\_control\_allow\_origin | Array | **Optional.** Specifies an array of origin URLs that may access the API. [(MDN docs)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Access-Control-Allow-Origin)
- access\_control\_allow\_credentials | Boolean | **Deprecated.** Indicates whether or not the actual request can be made using credentials. Defaults to `true`. [(MDN docs)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Access-Control-Allow-Credentials)
- access\_control\_allow\_headers | String | **Deprecated.** Used in response to a preflight request to indicate which HTTP headers can be used when making the actual request. Defaults to `Authorization`. [(MDN docs)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Access-Control-Allow-Headers)
- access\_control\_allow\_methods | String | **Deprecated.** Used in response to a preflight request to indicate which HTTP methods can be used when making the actual request. Defaults to `GET, POST, PUT, DELETE`. [(MDN docs)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Access-Control-Allow-Methods)
- environment | String | **Optional.** Used as suffix in TLS SNI extension name; default from constant `ApiEnvironment`, which is empty.
- http\_response\_headers | Dictionary | **Optional.** Additional headers to add to HTTP responses, for example `{"Strict-Transport-Security" = "max-age=31536000"}`. Defaults to none.
+ Name | Type | Description
+ ----------------------------------------|------------|----------------------------------
+ cert\_path | String | **Deprecated.** Path to the public key.
+ key\_path | String | **Deprecated.** Path to the private key.
+ ca\_path | String | **Deprecated.** Path to the CA certificate file.
+ ticket\_salt | String | **Optional.** Private key for [CSR auto-signing](06-distributed-monitoring.md#distributed-monitoring-setup-csr-auto-signing). **Required** for a signing master instance.
+ crl\_path | String | **Optional.** Path to the CRL file.
+ bind\_host | String | **Optional.** The IP address the api listener should be bound to. If not specified, the ApiListener is bound to `::` and listens for both IPv4 and IPv6 connections or to `0.0.0.0` if IPv6 is not supported by the operating system.
+ bind\_port | Number | **Optional.** The port the api listener should be bound to. Defaults to `5665`.
+ accept\_config | Boolean | **Optional.** Accept zone configuration. Defaults to `false`.
+ accept\_commands | Boolean | **Optional.** Accept remote commands. Defaults to `false`.
+ max\_anonymous\_clients | Number | **Optional.** Limit the number of anonymous client connections (not configured endpoints and signing requests).
+ cipher\_list | String | **Optional.** Cipher list that is allowed. For a list of available ciphers run `openssl ciphers`. Defaults to `ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256`.
+ tls\_protocolmin | String | **Optional.** Minimum TLS protocol version. Since v2.11, only `TLSv1.2` is supported. Defaults to `TLSv1.2`.
+ tls\_handshake\_timeout | Number | **Deprecated.** TLS Handshake timeout. Defaults to `10s`.
+ connect\_timeout | Number | **Optional.** Timeout for establishing new connections. Affects both incoming and outgoing connections. Within this time, the TCP and TLS handshakes must complete and either a HTTP request or an Icinga cluster connection must be initiated. Defaults to `15s`.
+ access\_control\_allow\_origin | Array | **Optional.** Specifies an array of origin URLs that may access the API. [(MDN docs)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Access-Control-Allow-Origin)
+ access\_control\_allow\_credentials | Boolean | **Deprecated.** Indicates whether or not the actual request can be made using credentials. Defaults to `true`. [(MDN docs)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Access-Control-Allow-Credentials)
+ access\_control\_allow\_headers | String | **Deprecated.** Used in response to a preflight request to indicate which HTTP headers can be used when making the actual request. Defaults to `Authorization`. [(MDN docs)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Access-Control-Allow-Headers)
+ access\_control\_allow\_methods | String | **Deprecated.** Used in response to a preflight request to indicate which HTTP methods can be used when making the actual request. Defaults to `GET, POST, PUT, DELETE`. [(MDN docs)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Access-Control-Allow-Methods)
+ environment | String | **Optional.** Used as suffix in TLS SNI extension name; default from constant `ApiEnvironment`, which is empty.
+ http\_response\_headers | Dictionary | **Optional.** Additional headers to add to HTTP responses, for example `{"Strict-Transport-Security" = "max-age=31536000"}`. Defaults to none.
+ enforce\_filter\_expression\_permission | Boolean | **Optional.** Enforce the `filter-expression` permission. Defaults to `false` until v2.17 for compatibility.
The attributes `access_control_allow_credentials`, `access_control_allow_headers` and `access_control_allow_methods`
are controlled by Icinga 2 and are not changeable by config any more.
diff --git a/doc/12-icinga2-api.md b/doc/12-icinga2-api.md
index 532da3b03..a42425376 100644
--- a/doc/12-icinga2-api.md
+++ b/doc/12-icinga2-api.md
@@ -299,6 +299,18 @@ Available permissions for specific URL endpoints:
types | /v1/types | Yes | 1
variables | /v1/variables | Yes | 1
+Available permissions that are not bound to specific URL endpoints:
+
+ Permissions | Description
+ ------------------------------|------------
+ filter-expression | Allows the user to provide their own [advanced filter expressions](12-icinga2-api.md#icinga2-api-advanced-filters).
+
+!!! warning
+
+ The `filter-expression` permission was introduced in v2.16.2 and is only enforced if the
+ [`enforce_filter_expression_permission` attribute of `ApiListener`](09-object-types.md#objecttype-apilistener)
+ is set to `true`. For compatibility reasons, this will not be enforced by default until v2.17.
+
The required actions or types can be replaced by using a wildcard match ("\*").
@@ -435,6 +447,16 @@ The syntax for these filters is the same like for [apply rule expressions](03-mo
The `filter` parameter can only be specified once, complex filters must
be defined once in the provided string value.
+!!! warning
+
+ In order to use these advanced filters, the `ApiUser` must be granted the `filter-expression` permission,
+ which should only be done for trusted users. The evaluation happens in the main Icinga 2 worker process and may be
+ abused for denial-of-service attacks, potentially crashing the Icinga 2 daemon.
+
+ Note that before v2.17, this permission is not enforced by default but only if the
+ [`enforce_filter_expression_permission` attribute of `ApiListener`](09-object-types.md#objecttype-apilistener)
+ is set accordingly.
+
> **Note**
>
> Filters used as URL parameter must be URL-encoded. The following examples
diff --git a/doc/16-upgrading-icinga-2.md b/doc/16-upgrading-icinga-2.md
index dbd9567ef..c94fe4b3a 100644
--- a/doc/16-upgrading-icinga-2.md
+++ b/doc/16-upgrading-icinga-2.md
@@ -8,6 +8,23 @@ Specific version upgrades are described below. Please note that version
updates are incremental. An upgrade from v2.6 to v2.8 requires to
follow the instructions for v2.7 too.
+## Upgrading to v2.16.2, v2.15.4, or v2.14.9
+
+### New `filter-expression` permission
+
+When using the Icinga 2 REST API, filter expressions are a powerful tool. However, that power can also be abused for
+denial-of-service attacks by authenticated users. Given that these filter expressions are Icinga 2 DSL expressions and
+their evaluation happens in the main Icinga 2 worker process, this may also crash the Icinga 2 daemon.
+
+In order to allow mitigating such problems, a new permission named `filter-expression` was added. **Before v2.17, this
+permission will not be enforced by default** due to the impact on existing installations. Nonetheless, we recommend
+reviewing your `ApiUser` configuration, explicitly allowing the new permission where necessary and enabling the
+enforcement of the permission using the [`enforce_filter_expression_permission` attribute of
+`ApiListener`](09-object-types.md#objecttype-apilistener). If an `ApiUser` makes use of the permission while the
+permission is not enforced yet, it will be logged. Thus, it is also possible to upgrade, observe the logs, grant the
+permission as needed or adapting API clients to avoid using filters if possible, and enable the enforcement at a later
+time.
+
## Upgrading to v2.16
### Migrating from ElasticsearchWriter to OTLPMetricsWriter
diff --git a/lib/remote/actionshandler.cpp b/lib/remote/actionshandler.cpp
index 76874cee4..40d6607f4 100644
--- a/lib/remote/actionshandler.cpp
+++ b/lib/remote/actionshandler.cpp
@@ -54,6 +54,9 @@ bool ActionsHandler::HandleRequest(
try {
objs = FilterUtility::GetFilterTargets(qd, params, user);
+ } catch (const MissingPermissionError& ex) {
+ HttpUtility::SendJsonError(response, params, 403, ex.what());
+ return true;
} catch (const std::exception& ex) {
HttpUtility::SendJsonError(response, params, 404,
"No objects found.",
diff --git a/lib/remote/apilistener.cpp b/lib/remote/apilistener.cpp
index aa13eaf56..2a358ee03 100644
--- a/lib/remote/apilistener.cpp
+++ b/lib/remote/apilistener.cpp
@@ -234,6 +234,18 @@ void ApiListener::OnAllConfigLoaded()
if (!m_LocalEndpoint)
BOOST_THROW_EXCEPTION(ScriptError("Endpoint object for '" + GetIdentity() + "' is missing.", GetDebugInfo()));
+
+ if (!GetEnforceFilterExpressionPermission()) {
+ Log(LogWarning, "ApiListener") << "Security notice:\n"
+ " Currently, all ApiUsers are allowed to use Icinga 2 DSL filter expressions\n"
+ " in API queries because enforce_filter_expression_permission is set to false.\n"
+ " This can pose a security risk as filters are evaluated within the Icinga 2\n"
+ " process and their complexity can be used for denial of service attacks. The\n"
+ " new '" << ApiUser::FilterExpressionPerm << "' permission can be used to allow this for individual\n"
+ " ApiUsers, which should only be granted to trusted users. It is recommended\n"
+ " to set enforce_filter_expression_permission to true to enforce the\n"
+ " permission. This will become the default in v2.17.";
+ }
}
/**
diff --git a/lib/remote/apilistener.ti b/lib/remote/apilistener.ti
index b3cd883cd..afa13f5ce 100644
--- a/lib/remote/apilistener.ti
+++ b/lib/remote/apilistener.ti
@@ -63,6 +63,10 @@ class ApiListener : ConfigObject
[no_user_modify] String identity;
[state, no_user_modify] Dictionary::Ptr last_failed_zones_stage_validation;
+
+ [config, no_user_modify] bool enforce_filter_expression_permission {
+ default {{{ return false; }}}
+ };
};
}
diff --git a/lib/remote/apiuser.hpp b/lib/remote/apiuser.hpp
index 0e6336ed4..b044e4abc 100644
--- a/lib/remote/apiuser.hpp
+++ b/lib/remote/apiuser.hpp
@@ -21,6 +21,8 @@ public:
static ApiUser::Ptr GetByClientCN(const String& cn);
static ApiUser::Ptr GetByAuthHeader(const String& auth_header);
+
+ static inline const String FilterExpressionPerm = "filter-expression";
};
}
diff --git a/lib/remote/deleteobjecthandler.cpp b/lib/remote/deleteobjecthandler.cpp
index a918d4a30..beecc2001 100644
--- a/lib/remote/deleteobjecthandler.cpp
+++ b/lib/remote/deleteobjecthandler.cpp
@@ -58,6 +58,9 @@ bool DeleteObjectHandler::HandleRequest(
try {
objs = FilterUtility::GetFilterTargets(qd, params, user);
+ } catch (const MissingPermissionError& ex) {
+ HttpUtility::SendJsonError(response, params, 403, ex.what());
+ return true;
} catch (const std::exception& ex) {
HttpUtility::SendJsonError(response, params, 404,
"No objects found.",
diff --git a/lib/remote/eventshandler.cpp b/lib/remote/eventshandler.cpp
index e1c354b96..545c3ba75 100644
--- a/lib/remote/eventshandler.cpp
+++ b/lib/remote/eventshandler.cpp
@@ -92,7 +92,18 @@ bool EventsHandler::HandleRequest(
}
}
- EventsSubscriber subscriber (std::move(eventTypes), HttpUtility::GetLastParameter(params, "filter"), l_ApiQuery);
+ String filter = "";
+ if (params && params->Contains("filter")) {
+ if (!FilterUtility::HasPermission(request.User(), ApiUser::FilterExpressionPerm, nullptr)) {
+ HttpUtility::SendJsonError(response, params, 403,
+ "Missing permission: " + ApiUser::FilterExpressionPerm);
+ return true;
+ }
+
+ filter = HttpUtility::GetLastParameter(params, "filter");
+ }
+
+ EventsSubscriber subscriber (std::move(eventTypes), std::move(filter), l_ApiQuery);
response.result(http::status::ok);
response.set(http::field::content_type, "application/json");
diff --git a/lib/remote/filterutility.cpp b/lib/remote/filterutility.cpp
index ac04b2395..ba342d257 100644
--- a/lib/remote/filterutility.cpp
+++ b/lib/remote/filterutility.cpp
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "remote/filterutility.hpp"
+#include "remote/apilistener.hpp"
#include "remote/httputility.hpp"
#include "config/applyrule.hpp"
#include "config/configcompiler.hpp"
@@ -300,6 +301,22 @@ bool FilterUtility::HasPermission(const ApiUser::Ptr& user, const String& permis
}
}
+ // Requiring the "filter-expression" permission to use any filter expression is an incompatible change. Therefore,
+ // there is a config option to configure whether that permission check is enforced or not. If it is not enforced,
+ // a message is logged, allowing the admin to determine which ApiUsers require this permission. This allows granting
+ // the permission and/or adapting clients as needed until this message does not show up anymore and then switching
+ // to enforcing the permission check.
+ if (!foundPermission && permission == ApiUser::FilterExpressionPerm) {
+ ApiListener::Ptr listener = ApiListener::GetInstance();
+ if (listener && !listener->GetEnforceFilterExpressionPermission()) {
+ Log(LogWarning, "FilterUtility") << "ApiUser '" << user->GetName()
+ << "' was allowed to use a filter expression despite missing the '" << ApiUser::FilterExpressionPerm
+ << "' permission due to ApiListener.enforce_filter_expression_permission = false.";
+
+ return true;
+ }
+ }
+
if (!foundPermission) {
Log(LogWarning, "FilterUtility")
<< "Missing permission: " << requiredPermission;
@@ -311,7 +328,7 @@ bool FilterUtility::HasPermission(const ApiUser::Ptr& user, const String& permis
void FilterUtility::CheckPermission(const ApiUser::Ptr& user, const String& permission, std::unique_ptr* permissionFilter)
{
if (!HasPermission(user, permission, permissionFilter)) {
- BOOST_THROW_EXCEPTION(ScriptError("Missing permission: " + permission.ToLower()));
+ BOOST_THROW_EXCEPTION(MissingPermissionError("Missing permission: " + permission.ToLower()));
}
}
@@ -386,6 +403,7 @@ std::vector FilterUtility::GetFilterTargets(const QueryDescription& qd, c
frame.PermChecker = permissionChecker;
if (query->Contains("filter")) {
+ CheckPermission(user, ApiUser::FilterExpressionPerm, nullptr);
String filter = HttpUtility::GetLastParameter(query, "filter");
std::unique_ptr ufilter = ConfigCompiler::CompileText("", filter);
Dictionary::Ptr filter_vars = query->Get("filter_vars");
diff --git a/lib/remote/filterutility.hpp b/lib/remote/filterutility.hpp
index 0f3a1b257..d952df6ee 100644
--- a/lib/remote/filterutility.hpp
+++ b/lib/remote/filterutility.hpp
@@ -62,6 +62,19 @@ public:
const Object::Ptr& target, const String& variableName = String());
};
+/**
+ * Exception to report a missing permission to an API user.
+ *
+ * IMPORTANT: The what() message is reported back to the user and MUST NOT contain sensitive information like names of
+ * objects they are not allowed to access. When using the exception, also pay attention that throwing the exception
+ * does not introduce a sidechannel. For example, it should not be returned if a user-specified object exists but the
+ * user is not allowed to access it, otherwise they would learn that the object exists.
+ */
+class MissingPermissionError : public ScriptError
+{
+ using ScriptError::ScriptError;
+};
+
}
#endif /* FILTERUTILITY_H */
diff --git a/lib/remote/modifyobjecthandler.cpp b/lib/remote/modifyobjecthandler.cpp
index 5d97919dd..3c0ae41ca 100644
--- a/lib/remote/modifyobjecthandler.cpp
+++ b/lib/remote/modifyobjecthandler.cpp
@@ -56,6 +56,9 @@ bool ModifyObjectHandler::HandleRequest(
try {
objs = FilterUtility::GetFilterTargets(qd, params, user);
+ } catch (const MissingPermissionError& ex) {
+ HttpUtility::SendJsonError(response, params, 403, ex.what());
+ return true;
} catch (const std::exception& ex) {
HttpUtility::SendJsonError(response, params, 404,
"No objects found.",
diff --git a/lib/remote/objectqueryhandler.cpp b/lib/remote/objectqueryhandler.cpp
index 2c81c6a73..ef73515c4 100644
--- a/lib/remote/objectqueryhandler.cpp
+++ b/lib/remote/objectqueryhandler.cpp
@@ -178,6 +178,9 @@ bool ObjectQueryHandler::HandleRequest(
try {
objs = FilterUtility::GetFilterTargets(qd, params, user);
+ } catch (const MissingPermissionError& ex) {
+ HttpUtility::SendJsonError(response, params, 403, ex.what());
+ return true;
} catch (const std::exception& ex) {
HttpUtility::SendJsonError(response, params, 404,
"No objects found.",
diff --git a/lib/remote/statushandler.cpp b/lib/remote/statushandler.cpp
index 1b0aa1717..4202b6a87 100644
--- a/lib/remote/statushandler.cpp
+++ b/lib/remote/statushandler.cpp
@@ -102,6 +102,9 @@ bool StatusHandler::HandleRequest(
try {
objs = FilterUtility::GetFilterTargets(qd, params, user);
+ } catch (const MissingPermissionError& ex) {
+ HttpUtility::SendJsonError(response, params, 403, ex.what());
+ return true;
} catch (const std::exception& ex) {
HttpUtility::SendJsonError(response, params, 404,
"No objects found.",
diff --git a/lib/remote/templatequeryhandler.cpp b/lib/remote/templatequeryhandler.cpp
index 1aaba7906..0684df681 100644
--- a/lib/remote/templatequeryhandler.cpp
+++ b/lib/remote/templatequeryhandler.cpp
@@ -119,6 +119,9 @@ bool TemplateQueryHandler::HandleRequest(
try {
objs = FilterUtility::GetFilterTargets(qd, params, user, "tmpl");
+ } catch (const MissingPermissionError& ex) {
+ HttpUtility::SendJsonError(response, params, 403, ex.what());
+ return true;
} catch (const std::exception& ex) {
HttpUtility::SendJsonError(response, params, 404,
"No templates found.",
diff --git a/lib/remote/typequeryhandler.cpp b/lib/remote/typequeryhandler.cpp
index 203dfbf41..9460070a2 100644
--- a/lib/remote/typequeryhandler.cpp
+++ b/lib/remote/typequeryhandler.cpp
@@ -81,6 +81,9 @@ bool TypeQueryHandler::HandleRequest(
try {
objs = FilterUtility::GetFilterTargets(qd, params, user);
+ } catch (const MissingPermissionError& ex) {
+ HttpUtility::SendJsonError(response, params, 403, ex.what());
+ return true;
} catch (const std::exception& ex) {
HttpUtility::SendJsonError(response, params, 404,
"No objects found.",
diff --git a/lib/remote/variablequeryhandler.cpp b/lib/remote/variablequeryhandler.cpp
index 505983c45..c5634de3b 100644
--- a/lib/remote/variablequeryhandler.cpp
+++ b/lib/remote/variablequeryhandler.cpp
@@ -91,6 +91,9 @@ bool VariableQueryHandler::HandleRequest(
try {
objs = FilterUtility::GetFilterTargets(qd, params, user, "variable");
+ } catch (const MissingPermissionError& ex) {
+ HttpUtility::SendJsonError(response, params, 403, ex.what());
+ return true;
} catch (const std::exception& ex) {
HttpUtility::SendJsonError(response, params, 404,
"No variables found.",
diff --git a/test/remote-filterutility.cpp b/test/remote-filterutility.cpp
index aa5441d95..be0634344 100644
--- a/test/remote-filterutility.cpp
+++ b/test/remote-filterutility.cpp
@@ -15,6 +15,60 @@ BOOST_AUTO_TEST_SUITE(remote_filterutility,
*boost::unit_test::label("config"))
// clang-format on
+BOOST_FIXTURE_TEST_CASE(filter_expression_permission, IcingaApplicationFixture)
+{
+ auto createObjects = []() {
+ String config = R"CONFIG({
+object CheckCommand "dummy" {
+ command = "/bin/echo"
+}
+
+object ApiUser "withFilterPermission" {
+ permissions = [ "objects/query/*", "filter-expression" ]
+}
+
+object ApiUser "withoutFilterPermission" {
+ permissions = [ "objects/query/*" ]
+}
+
+object Host "host1" {
+ address = "host1"
+ check_command = "dummy"
+}
+})CONFIG";
+ std::unique_ptr expr = ConfigCompiler::CompileText("", config);
+ expr->Evaluate(*ScriptFrame::GetCurrentFrame());
+ };
+
+ ConfigItem::RunWithActivationContext(new Function("CreateTestObjects", createObjects));
+
+ auto userWithPerm = ApiUser::GetByName("withFilterPermission");
+ auto userWithoutPerm = ApiUser::GetByName("withoutFilterPermission");
+
+ QueryDescription qd;
+ qd.Types.insert("Host");
+ 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", "true");
+
+ std::vector objs;
+ BOOST_REQUIRE_NO_THROW(objs = FilterUtility::GetFilterTargets(qd, queryParams, userWithPerm));
+ BOOST_CHECK_EQUAL(objs.size(), 1);
+
+ BOOST_CHECK_EXCEPTION(FilterUtility::GetFilterTargets(qd, queryParams, userWithoutPerm), std::exception,
+ [](const std::exception& ex) {
+ boost::test_tools::assertion_result result{std::string_view(ex.what()) == "Missing permission: filter-expression"};
+ result.message() << "got exception: " << ex.what();
+ return result;
+ });
+}
+
BOOST_FIXTURE_TEST_CASE(safe_function_permissions, IcingaApplicationFixture)
{
auto createObjects = []() {
@@ -29,6 +83,7 @@ object ApiUser "allPermissionsUser" {
object ApiUser "permissionFilterUser" {
permissions = [
+ "filter-expression",
{
permission = "objects/query/Host"
filter = {{ host.name == {{{host1}}} }}
@@ -161,6 +216,7 @@ object ApiUser "allPermissionsUser" {
object ApiUser "permissionFilterUser" {
permissions = [
+ "filter-expression",
"objects/query/Host",
{
permission = "variables"
@@ -170,7 +226,10 @@ object ApiUser "permissionFilterUser" {
}
object ApiUser "noVariablePermUser" {
- permissions = [ "objects/query/Host" ]
+ permissions = [
+ "filter-expression",
+ "objects/query/Host",
+ ]
}
object Host "host1" {