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" {