Skip to content

Commit aeeef97

Browse files
crazyscientistJenkins
authored andcommitted
22104 FIX REST API: reject rule values in an outdated format when creating or editing rules
When creating or editing a rule via the REST API, Checkmk could store a value that did not match the `value_raw` reported in the API response. During validation the `value_raw` field is migrated to the current format first and only then checked. Depending on the ruleset's migration logic, validation could therefore succeed while the original, unmigrated value was persisted to disk. Later on, the consuming plugin could stumble over the unmigrated value. For example, a "Periodic service discovery" rule created this way could send the "Check_MK Discovery" service into a crash. The REST API now rejects such requests with an HTTP 400 error instead of silently migrating the value. Persisting a migrated value would store data the client is unaware of: unlike the GUI - which renders the migrated value before you save it - an API client never gets to see the migration, and the `value_raw` reported back would no longer match the input. If you receive an "Outdated value format" error, migrate the affected `value_raw` to the current format - for example by opening and saving the rule once in the GUI, then reading it back via the REST API - and submit the migrated value. Change-Id: I71bdaf99237037771c836d59ba34751c6751eaf5 Jira: CMK-35332
1 parent ef68a6c commit aeeef97

3 files changed

Lines changed: 128 additions & 2 deletions

File tree

.werks/22104.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
[//]: # (werk v3)
2+
# REST API: reject rule values in an outdated format when creating or editing rules
3+
4+
key | value
5+
---------- | ---
6+
date | 2026-08-12T00:00:00.000000+00:00
7+
version | 3.0.0b1
8+
class | fix
9+
edition | community
10+
component | rest-api
11+
level | 1
12+
compatible | no
13+
14+
When creating or editing a rule via the REST API, Checkmk could store a value
15+
that did not match the `value_raw` reported in the API response.
16+
17+
During validation the `value_raw` field is migrated to the current format first
18+
and only then checked. Depending on the ruleset's migration logic, validation
19+
could therefore succeed while the original, unmigrated value was persisted to
20+
disk. Later on, the consuming plugin could stumble over the unmigrated value.
21+
For example, a "Periodic service discovery" rule created this way could send the
22+
"Check_MK Discovery" service into a crash.
23+
24+
The REST API now rejects such requests with an HTTP 400 error instead of silently
25+
migrating the value. Persisting a migrated value would store data the client is
26+
unaware of: unlike the GUI - which renders the migrated value before you save it -
27+
an API client never gets to see the migration, and the `value_raw` reported back
28+
would no longer match the input.
29+
30+
If you receive an "Outdated value format" error, migrate the affected `value_raw`
31+
to the current format - for example by opening and saving the rule once in the
32+
GUI, then reading it back via the REST API - and submit the migrated value.

cmk/gui/openapi/api_endpoints/rule/_utils.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# This file is part of Checkmk (https://checkmk.com). It is subject to the terms and
44
# conditions defined in the file COPYING, which is part of this source code package.
55

6+
import copy
67
import dataclasses
78
from collections.abc import Mapping, Sequence
89
from typing import Literal
@@ -272,7 +273,20 @@ def label_groups_to_api(label_groups: LabelGroups) -> list[LabelGroupConditionMo
272273

273274

274275
def validate_value(ruleset: Ruleset, value: RuleValue) -> None:
275-
"""Validate a rule value via the form spec, falling back to the legacy valuespec."""
276+
"""Validate a rule value via the form spec, falling back to the legacy valuespec.
277+
278+
The value is persisted verbatim, so it must already be in the current format. A value
279+
that only becomes valid after migration is rejected instead of being migrated silently:
280+
unlike the GUI - which renders the migrated value before the user saves it - the API
281+
client never gets to see such a change, so persisting it would store something the
282+
client is unaware of (and the ``value_raw`` reported back would no longer match the
283+
input).
284+
285+
The FormSpec path validates the raw value without migration (``migrate_values=False``),
286+
so an outdated value is already rejected there. The legacy valuespec validates the
287+
migrated value, hence we additionally reject values whose migrated form differs from
288+
the input.
289+
"""
276290
# FormSpec validation
277291
try:
278292
if problems := get_visitor(
@@ -301,6 +315,20 @@ def validate_value(ruleset: Ruleset, value: RuleValue) -> None:
301315

302316
raise ProblemException(status=400, title=title, detail=strip_tags(exc.message))
303317

318+
# Reject values that are not already in the current format. The legacy valuespec
319+
# validates the migrated value, so a value in an outdated format can pass validation
320+
# above. We deep-copy before migrating because some migrations mutate their input.
321+
if valuespec.transform_value(copy.deepcopy(value)) != value:
322+
raise ProblemException(
323+
status=400,
324+
title="Outdated value format",
325+
detail=(
326+
"The provided 'value_raw' is in an outdated format. Please migrate it to the "
327+
"current format - for example by opening and saving the rule in the GUI - and "
328+
"send the migrated value."
329+
),
330+
)
331+
304332

305333
def get_rule_by_id(
306334
tree: FolderTree, rule_uuid: str, all_rulesets: AllRulesets | None = None

tests/openapi/test_openapi_rules.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,9 @@ def test_create_rule_with_string_value(clients: ClientRegistry) -> None:
278278
assert resp.json["extensions"]["value_raw"] == "'d,u,r,f,s'"
279279

280280

281-
def test_create_rule_stores_migrated_value(clients: ClientRegistry) -> None:
281+
def test_create_rule_rejects_outdated_form_spec_value(clients: ClientRegistry) -> None:
282+
# A form spec value in an outdated format is validated without migration
283+
# (migrate_values=False) and therefore rejected instead of migrated silently (Werk 22104).
282284
ruleset = "diskstat_inventory"
283285
resp = clients.Rule.create(
284286
ruleset=ruleset,
@@ -291,6 +293,70 @@ def test_create_rule_stores_migrated_value(clients: ClientRegistry) -> None:
291293
resp.assert_status_code(400)
292294

293295

296+
# A "Periodic service discovery" value using the ancient integer rediscovery mode (0). The
297+
# ruleset would migrate this to a full 'custom' settings dict. The REST API must not persist
298+
# such a migrated value silently (unlike the GUI, the client never sees the migration), so
299+
# it rejects the outdated format instead (Werk 22104).
300+
LEGACY_VALUE_RAW = """{
301+
'check_interval': 120.0,
302+
'severity_unmonitored': 1,
303+
'severity_vanished': 0,
304+
'severity_changed_service_labels': 0,
305+
'severity_new_host_label': 1,
306+
'inventory_rediscovery': {
307+
'group_time': 900,
308+
'excluded_time': [],
309+
'activation': True,
310+
'mode': 0,
311+
},
312+
}"""
313+
314+
315+
def test_create_rule_rejects_outdated_legacy_value(clients: ClientRegistry) -> None:
316+
# A legacy value that only passes validation after migration must be rejected on create,
317+
# not persisted in migrated form (Werk 22104).
318+
resp = clients.Rule.create(
319+
ruleset="periodic_discovery",
320+
folder="/",
321+
properties={"description": "legacy mode", "disabled": False},
322+
value_raw=LEGACY_VALUE_RAW,
323+
conditions={},
324+
expect_ok=False,
325+
)
326+
resp.assert_status_code(400)
327+
328+
ruleset = clients.Ruleset.get(ruleset_id="periodic_discovery")
329+
assert ruleset.json["extensions"]["number_of_rules"] == 0
330+
331+
332+
def test_edit_rule_rejects_outdated_legacy_value(clients: ClientRegistry) -> None:
333+
# Like test_create_rule_rejects_outdated_legacy_value, but for the edit (PUT) code path:
334+
# editing a rule to a legacy value that only passes validation after migration must be
335+
# rejected, and must leave the stored value untouched (Werk 22104).
336+
created = clients.Rule.create(
337+
ruleset="periodic_discovery",
338+
folder="/",
339+
properties={"description": "current value", "disabled": False},
340+
value_raw=DEFAULT_VALUE_RAW,
341+
conditions={},
342+
)
343+
344+
resp = clients.Rule.edit(
345+
rule_id=created.json["id"],
346+
value_raw=LEGACY_VALUE_RAW,
347+
properties={"description": "legacy mode", "disabled": False},
348+
conditions={},
349+
expect_ok=False,
350+
)
351+
resp.assert_status_code(400)
352+
353+
# The stored value must be unchanged: still the current-format value, not migrated.
354+
rules_mk = paths.omd_root / "etc/check_mk/conf.d/wato/rules.mk"
355+
environ = load_mk_file(rules_mk, default={}, lock=False)
356+
value = environ["periodic_discovery"][0]["value"] # type: ignore[index]
357+
assert "inventory_rediscovery" not in value
358+
359+
294360
def test_openapi_list_rules(
295361
clients: ClientRegistry,
296362
new_rule: tuple[Response, dict[str, typing.Any]],

0 commit comments

Comments
 (0)