Skip to content

Commit 046c44c

Browse files
committed
quick_setup: Enforce permission check
This change allows permission check for Quick Setup actions * Allows to specify a required permission on the action level * If required permission is set, then validate it during Quick Setup requests * Added permissions required on AWS, Azure and GCP Quick Setups * Enforce the permissions of the Quick Setup endpoints according to the permissions declared in the actions. CMK-27975 Change-Id: I963d460deac564dadaea25c56940998cc5ebefb4
1 parent afbb2b5 commit 046c44c

6 files changed

Lines changed: 150 additions & 4 deletions

File tree

cmk/gui/openapi/endpoints/quick_setup/__init__.py

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
from cmk.gui.quick_setup.v0_unstable.predefined import build_formspec_map_from_stages
6464
from cmk.gui.quick_setup.v0_unstable.predefined._common import _find_id_in_form_data
6565
from cmk.gui.quick_setup.v0_unstable.setups import (
66+
get_all_permissions,
6667
QuickSetupActionMode,
6768
QuickSetupBackgroundAction,
6869
QuickSetupBackgroundStageAction,
@@ -151,6 +152,7 @@ class QuickSetupMode(StrEnum):
151152
query_params=[QUICKSETUP_MODE, QUICKSETUP_OBJECT_ID],
152153
path_params=[QUICKSETUP_ID],
153154
response_schema=QuickSetupResponse,
155+
additional_status_codes=[403],
154156
)
155157
def get_guided_stages_or_overview_stages(params: Mapping[str, Any]) -> Response:
156158
"""Get guided stages or overview stages"""
@@ -166,6 +168,15 @@ def get_guided_stages_or_overview_stages(params: Mapping[str, Any]) -> Response:
166168
detail=f"Quick setup with id '{quick_setup_id}' does not exist.",
167169
)
168170

171+
permissions = get_all_permissions(quick_setup)
172+
173+
if permissions is not None and not all(user.may(perm) for perm in permissions):
174+
return _serve_error(
175+
title="Action not allowed",
176+
detail=f"Requires {', '.join(f"'{p}'" for p in permissions)} permissions.",
177+
status_code=403,
178+
)
179+
169180
mode: QuickSetupMode = params["mode"]
170181
prefill_data: ParsedFormData | None = None
171182
if object_id := params["object_id"]:
@@ -205,6 +216,7 @@ def get_guided_stages_or_overview_stages(params: Mapping[str, Any]) -> Response:
205216
path_params=[QUICKSETUP_ID, STAGE_INDEX],
206217
query_params=[QUICKSETUP_OBJECT_ID],
207218
response_schema=QuickSetupStageStructure,
219+
additional_status_codes=[403],
208220
)
209221
def quick_setup_get_stage_structure(params: Mapping[str, Any]) -> Response:
210222
"""Get a Quick setup stage structure"""
@@ -220,6 +232,15 @@ def quick_setup_get_stage_structure(params: Mapping[str, Any]) -> Response:
220232
detail=f"Quick setup with id '{quick_setup_id}' does not exist.",
221233
)
222234

235+
permissions = get_all_permissions(quick_setup)
236+
237+
if permissions is not None and not all(user.may(perm) for perm in permissions):
238+
return _serve_error(
239+
title="Action not allowed",
240+
detail=f"Requires {', '.join(f"'{p}'" for p in permissions)} permissions.",
241+
status_code=403,
242+
)
243+
223244
prefill_data: ParsedFormData | None = None
224245
if object_id:
225246
prefill_data = quick_setup.load_data(object_id)
@@ -246,13 +267,20 @@ def quick_setup_get_stage_structure(params: Mapping[str, Any]) -> Response:
246267
303: "The stage validation and recap action has been started in the background. "
247268
"Redirecting to the 'Get background job status snapshot' endpoint."
248269
},
249-
additional_status_codes=[303],
270+
additional_status_codes=[303, 403],
250271
path_params=[QUICKSETUP_ID],
251272
request_schema=QuickSetupStageActionRequest,
252273
response_schema=QuickSetupStageActionResponse,
253274
)
254275
def quicksetup_run_stage_action(params: Mapping[str, Any]) -> Response:
255-
"""Run a Quick setup stage validation and recap action"""
276+
"""Run a Quick setup stage validation and recap action
277+
278+
This endpoint performs permission validation but since permissions depend on dynamic actions
279+
and each action has its own required permission, they cannot be statically defined. If the
280+
required permissions for an action are not met, the endpoint returns a 403 Forbidden error
281+
informing the action ID and the missing permission.
282+
"""
283+
256284
language = user.language
257285
body = params["body"]
258286
quick_setup_id = params["quick_setup_id"]
@@ -267,6 +295,15 @@ def quicksetup_run_stage_action(params: Mapping[str, Any]) -> Response:
267295
stage_index = StageIndex(len(body["stages"]) - 1)
268296
stage_action = matching_stage_action(quick_setup.stages[stage_index](), stage_action_id)
269297

298+
if stage_action.permissions is not None and not all(
299+
user.may(perm) for perm in stage_action.permissions
300+
):
301+
return _serve_error(
302+
title="Action not allowed",
303+
detail=f"Action with id '{stage_action_id}' requires {', '.join(f"'{x}'" for x in stage_action.permissions)} permissions.",
304+
status_code=403,
305+
)
306+
270307
built_stages = [stage() for stage in quick_setup.stages[: stage_index + 1]]
271308
form_spec_map = build_formspec_map_from_stages(built_stages)
272309
stages_raw_formspecs = [RawFormData(stage["form_data"]) for stage in body["stages"]]
@@ -384,7 +421,7 @@ def fetch_quick_setup_stage_action_result(params: Mapping[str, Any]) -> Response
384421
tag_group="Checkmk Internal",
385422
path_params=[QUICKSETUP_ID],
386423
query_params=[QUICKSETUP_MODE],
387-
additional_status_codes=[201, 303, 429],
424+
additional_status_codes=[201, 303, 403, 429],
388425
status_descriptions={
389426
303: "The validation and complete action has been started in the background. "
390427
"Redirecting to the 'Get background job status snapshot' endpoint.",
@@ -405,7 +442,7 @@ def quick_setup_run_action(params: Mapping[str, Any]) -> Response:
405442
tag_group="Checkmk Internal",
406443
path_params=[QUICKSETUP_ID],
407444
query_params=[QUICKSETUP_OBJECT_ID_REQUIRED],
408-
additional_status_codes=[201, 303, 429],
445+
additional_status_codes=[201, 303, 403, 429],
409446
status_descriptions={
410447
303: "The validation and complete action has been started in the background. "
411448
"Redirecting to the 'Get background job status snapshot' endpoint.",
@@ -450,6 +487,13 @@ def complete_quick_setup_action(params: Mapping[str, Any], mode: QuickSetupActio
450487
detail=f"Action with id '{action_id}' does not exist.",
451488
)
452489

490+
if action.permissions is not None and not all(user.may(perm) for perm in action.permissions):
491+
return _serve_error(
492+
title="Action not allowed",
493+
detail=f"Action with id '{action_id}' requires {', '.join(f"'{x}'" for x in action.permissions)} permissions.",
494+
status_code=403,
495+
)
496+
453497
form_spec_map = build_formspec_map_from_stages([stage() for stage in quick_setup.stages])
454498
errors = validate_stages_form_data(
455499
stages_raw_form_data=[RawFormData(stage["form_data"]) for stage in body["stages"]],

cmk/gui/quick_setup/config_setups/aws/stages.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ def prepare_aws() -> QuickSetupStage:
132132
],
133133
recap=[recaps.recaps_form_spec],
134134
next_button_label=_("Configure host and regions"),
135+
permissions=["wato.passwords"],
135136
)
136137
],
137138
)
@@ -163,6 +164,7 @@ def configure_host_and_regions() -> QuickSetupStage:
163164
],
164165
recap=[recaps.recaps_form_spec],
165166
next_button_label=_("Configure services to monitor"),
167+
permissions=["wato.hosts"],
166168
)
167169
],
168170
prev_button_label=PREV_BUTTON_LABEL,
@@ -445,6 +447,7 @@ def aws_transform_to_disk(params: Mapping[str, object]) -> Mapping[str, object]:
445447
action=action,
446448
icon=QuickSetupActionButtonIcon(name="save-to-services"),
447449
custom_validators=[qs_validators.validate_host_name_doesnt_exists],
450+
permissions=["wato.passwords", "wato.rulesets", "wato.hosts"],
448451
),
449452
],
450453
)

cmk/gui/quick_setup/config_setups/azure/stages.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ def configure_authentication() -> QuickSetupStage:
155155
],
156156
recap=[recaps.recaps_form_spec],
157157
next_button_label=_("Configure host and authority"),
158+
permissions=["wato.passwords"],
158159
),
159160
],
160161
)
@@ -179,6 +180,7 @@ def configure_host_and_authority() -> QuickSetupStage:
179180
],
180181
recap=[recaps.recaps_form_spec],
181182
next_button_label=_("Configure services to monitor"),
183+
permissions=["wato.hosts"],
182184
),
183185
],
184186
prev_button_label=PREV_BUTTON_LABEL,
@@ -356,6 +358,7 @@ def azure_collect_params(
356358
label=_("Save & go to Activate changes"),
357359
icon=QuickSetupActionButtonIcon(name="save-to-services"),
358360
action=action,
361+
permissions=["wato.passwords", "wato.rulesets", "wato.hosts"],
359362
),
360363
],
361364
)

cmk/gui/quick_setup/config_setups/gcp/stages.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ def configure_authentication() -> QuickSetupStage:
130130
],
131131
recap=[recaps.recaps_form_spec],
132132
next_button_label=_("Configure host"),
133+
permissions=["wato.passwords"],
133134
)
134135
],
135136
)
@@ -152,6 +153,7 @@ def configure_host() -> QuickSetupStage:
152153
],
153154
recap=[recaps.recaps_form_spec],
154155
next_button_label=_("Configure services to monitor"),
156+
permissions=["wato.hosts"],
155157
)
156158
],
157159
prev_button_label=PREV_BUTTON_LABEL,
@@ -345,6 +347,7 @@ def gcp_collect_params(
345347
label=_("Save & go to Activate changes"),
346348
icon=QuickSetupActionButtonIcon(name="save-to-services"),
347349
action=action,
350+
permissions=["wato.passwords", "wato.rulesets", "wato.hosts"],
348351
),
349352
],
350353
)

cmk/gui/quick_setup/v0_unstable/setups.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,18 @@ class QuickSetupStageAction:
8383
8484
load_wait_label:
8585
The label of the loading spinner. If not set, the default label is used.
86+
87+
permissions:
88+
The required permission name to be checked by the Quick Setup endpoint. By default
89+
it is set to None and no permission check is performed
8690
"""
8791

8892
id: ActionId
8993
custom_validators: Iterable[CallableValidator]
9094
recap: Iterable[CallableRecap]
9195
next_button_label: str | None = None
9296
load_wait_label: str | None = None
97+
permissions: list[str] | None = None
9398

9499

95100
@dataclass(frozen=True, kw_only=True)
@@ -181,13 +186,17 @@ class QuickSetupAction:
181186
182187
Therefore, relevant custom validators should be included again here (especially
183188
if the 'overview' mode is enabled)
189+
permissions:
190+
The required permission name to be checked by the Quick Setup endpoint. By default
191+
it is set to None and no permission check is performed
184192
"""
185193

186194
id: ActionId
187195
label: str
188196
action: CallableAction
189197
icon: QuickSetupActionButtonIcon | None = None
190198
custom_validators: Iterable[CallableValidator] = ()
199+
permissions: list[str] | None = None
191200

192201

193202
@dataclass(frozen=True, kw_only=True)
@@ -205,3 +214,17 @@ class QuickSetup:
205214
stages: Sequence[Callable[[], QuickSetupStage]]
206215
actions: Sequence[QuickSetupAction]
207216
load_data: Callable[[str], ParsedFormData | None] = lambda _: None
217+
218+
219+
def get_all_permissions(quick_setup: QuickSetup) -> list[str] | None:
220+
action_sources = [stage() for stage in quick_setup.stages] + [quick_setup]
221+
permissions = list(
222+
{
223+
perm
224+
for source in action_sources
225+
for action in source.actions
226+
if action.permissions
227+
for perm in action.permissions
228+
}
229+
)
230+
return permissions or None

tests/unit/cmk/gui/openapi/test_openapi_quick_setup.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,3 +743,73 @@ def test_openapi_background_job_setup_action(self, clients: ClientRegistry) -> N
743743
expect_ok=True,
744744
)
745745
assert "objects/background_job" in resp.headers["Location"]
746+
747+
748+
@pytest.mark.parametrize(
749+
"perm",
750+
[(["general.use"]), (None)],
751+
)
752+
def test_validate_permission(clients: ClientRegistry, perm: list[str] | None) -> None:
753+
register_quick_setup(
754+
setup_stages=[
755+
lambda: QuickSetupStage(
756+
title="stage1",
757+
configure_components=[
758+
widgets.unique_id_formspec_wrapper(Title("account name")),
759+
],
760+
actions=[
761+
QuickSetupStageAction(
762+
id=ActionId("action"),
763+
custom_validators=[],
764+
recap=[recaps.recaps_form_spec],
765+
next_button_label="Next",
766+
permissions=perm,
767+
)
768+
],
769+
),
770+
],
771+
)
772+
res = clients.QuickSetup.run_stage_action(
773+
quick_setup_id="quick_setup_test",
774+
stage_action_id="action",
775+
stages=[{"form_data": {UniqueFormSpecIDStr: {UniqueBundleIDStr: "test_account_name"}}}],
776+
)
777+
assert res.json["validation_errors"] is None
778+
assert res.json["background_job_exception"] is None
779+
assert res.json["stage_recap"][0]["id"] == "formspec_unique_id"
780+
assert res.json["stage_recap"][0]["form_spec"]["data"]["bundle_id"] == "test_account_name"
781+
782+
783+
def test_fail_validate_permission(clients: ClientRegistry) -> None:
784+
register_quick_setup(
785+
setup_stages=[
786+
lambda: QuickSetupStage(
787+
title="stage1",
788+
configure_components=[
789+
widgets.unique_id_formspec_wrapper(Title("account name")),
790+
],
791+
actions=[
792+
QuickSetupStageAction(
793+
id=ActionId("action"),
794+
custom_validators=[],
795+
recap=[recaps.recaps_form_spec],
796+
next_button_label="Next",
797+
permissions=["impossible_permisson"],
798+
)
799+
],
800+
),
801+
],
802+
)
803+
resp = clients.QuickSetup.run_stage_action(
804+
quick_setup_id="quick_setup_test",
805+
stage_action_id="action",
806+
stages=[{"form_data": {UniqueFormSpecIDStr: {UniqueBundleIDStr: "test_account_name"}}}],
807+
expect_ok=False,
808+
)
809+
810+
assert resp.status_code == 403
811+
assert resp.json["title"] == "Action not allowed"
812+
assert (
813+
resp.json["detail"]
814+
== "Action with id 'action' requires 'impossible_permisson' permissions."
815+
)

0 commit comments

Comments
 (0)