Skip to content

Commit c0d3f35

Browse files
committed
download_agents: serve shipped plugin agent files without login
The download handler introduced for agent files of plugin families (lib/python3/cmk/plugins/<family>/agents/) required a session and the 'wato.download_agents' permission, while the very same kind of file below share/check_mk/agents is served by the static Apache alias without any authentication. That regressed unauthenticated installation flows. Register the handler under 'noauth:' so it matches the alias again. Since authentication is decided before the page is called, this has to be a second instance of the same page class: families discovered below local/ (e.g. installed via MKP) are not shipped content and keep requiring login and permission, served by 'download_local_agent_plugin'. Their sections on the download page are labelled accordingly. Jira-Ref: CMK-36093 Change-Id: I657ef2f4551e82d701c5cca1245eba9cba086a85
1 parent 0df7b97 commit c0d3f35

4 files changed

Lines changed: 257 additions & 48 deletions

File tree

.werks/20126.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[//]: # (werk v3)
2+
# Agent files of shipped plugins are downloadable without a login again
3+
4+
key | value
5+
---------- | ---
6+
date | 2026-08-07T10:45:13.430386+00:00
7+
version | 2.5.0p12
8+
class | fix
9+
edition | community
10+
component | wato
11+
level | 1
12+
compatible | yes
13+
14+
Werk #20120 started serving the agent files provided by monitoring plugins — for example the Oracle plugin's `mk-oracle` files — through a dedicated download handler.
15+
That handler required a Checkmk login and the _Download agents_ permission, while the agent files below `share/check_mk/agents` have always been served without either.
16+
Unauthenticated installation flows fetching such a file got an error instead of the file.
17+
18+
Files provided by the monitoring plugins shipped with your Checkmk version are now served without a login again.
19+
20+
Files provided by locally installed monitoring plugins — for example from an extension package (MKP) — still require a login and the _Download agents_ permission.
21+
The download page marks their sections accordingly.

cmk/gui/wato/pages/download_agents.py

Lines changed: 98 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010
import abc
1111
import fnmatch
1212
import os
13-
from collections.abc import Callable, Collection, Generator, Iterable, Iterator, Mapping, Sequence
14-
from functools import cached_property
13+
from collections.abc import Callable, Collection, Generator, Iterator, Mapping, Sequence
14+
from dataclasses import dataclass
15+
from functools import cached_property, lru_cache
1516
from pathlib import Path
17+
from typing import Final
1618

1719
import cmk.utils.paths
1820
import cmk.utils.render
@@ -39,41 +41,77 @@
3941
from cmk.gui.watolib.hosts_and_folders import folder_preserving_link
4042
from cmk.gui.watolib.mode import ModeRegistry, WatoMode
4143

42-
# Page name of the GUI handler that streams agent plugin files which live outside
44+
# Page names of the GUI handlers that stream agent plugin files which live outside
4345
# the statically served share/check_mk/agents tree (e.g. cmk/plugins/<family>/agents/).
46+
# Files shipped with the version are served without authentication, just like the files
47+
# below the statically served tree. Files of locally installed plugins are not.
4448
DOWNLOAD_AGENT_PLUGIN_PAGE = "download_agent_plugin"
49+
DOWNLOAD_LOCAL_AGENT_PLUGIN_PAGE = "download_local_agent_plugin"
4550

4651

4752
def register(page_registry: PageRegistry, mode_registry: ModeRegistry) -> None:
4853
mode_registry.register(ModeDownloadAgentsOther)
4954
mode_registry.register(ModeDownloadAgentsWindows)
5055
mode_registry.register(ModeDownloadAgentsLinux)
51-
page_registry.register(PageEndpoint(DOWNLOAD_AGENT_PLUGIN_PAGE, PageDownloadAgentPlugin()))
5256

57+
# The endpoints handing out the files need to filter for allowed ones themselves!
58+
# Bonus: fills the cache of _plugin_family_agent_dirs at apache load.
59+
available_dirs = [d.path for d in _plugin_family_agent_dirs()]
60+
page_registry.register(
61+
PageEndpoint(
62+
f"noauth:{DOWNLOAD_AGENT_PLUGIN_PAGE}",
63+
PageDownloadAgentPlugin(
64+
[p for p in available_dirs if not p.is_relative_to(cmk.utils.paths.local_root)],
65+
require_permission=False,
66+
),
67+
)
68+
)
69+
page_registry.register(
70+
PageEndpoint(
71+
DOWNLOAD_LOCAL_AGENT_PLUGIN_PAGE,
72+
PageDownloadAgentPlugin(
73+
available_dirs,
74+
require_permission=True,
75+
),
76+
)
77+
)
5378

54-
def _plugin_family_agent_titles() -> Iterable[tuple[Path, str]]:
55-
"""Map each plugin family agents directory to a display title.
79+
80+
@dataclass(frozen=True)
81+
class PluginFamilyAgentDir:
82+
"""An agent plugin directory of a single plugin family (cmk.bakery.v2).
83+
84+
These live under lib/python3/cmk/plugins/<family>/agents/ - i.e. outside the
85+
statically served share/check_mk/agents tree - so files found here must be
86+
downloaded through a GUI handler, not the Apache alias.
87+
"""
88+
89+
path: Path
90+
title: str
91+
is_local: bool
92+
93+
94+
@lru_cache # This is based on python imports and thus never changes for a running process
95+
def _plugin_family_agent_dirs() -> Sequence[PluginFamilyAgentDir]:
96+
"""Discover the agent plugin directory of every plugin family.
5697
5798
``discover_families`` returns keys like ``cmk.plugins.oracle``; we use the
5899
last dotted component ("oracle") as a human readable section title ("Oracle")
59100
so that files of different families are not all lumped under one generic
60101
"Agents" header on the download page.
102+
103+
It also finds the families installed below ``local/`` (e.g. via MKP). Those are
104+
not shipped with the version, so they are marked to be handled separately.
61105
"""
62-
return (
63-
(Path(family_path, AGENT_PLUGINS_FOLDER), family.split(".")[-1].capitalize())
106+
return [
107+
PluginFamilyAgentDir(
108+
path=Path(family_path, AGENT_PLUGINS_FOLDER),
109+
title=family.split(".")[-1].capitalize(),
110+
is_local=Path(family_path).is_relative_to(cmk.utils.paths.local_root),
111+
)
64112
for family, family_paths in sorted(discover_families(raise_errors=False).items())
65113
for family_path in family_paths
66-
)
67-
68-
69-
def _plugin_family_agent_dirs() -> Sequence[Path]:
70-
"""Agent plugin directories grouped by plugin family (cmk.bakery.v2).
71-
72-
These live under lib/python3/cmk/plugins/<family>/agents/ - i.e. outside the
73-
statically served share/check_mk/agents tree - so files found here must be
74-
downloaded through the GUI handler, not the Apache alias.
75-
"""
76-
return [p for p, _t in _plugin_family_agent_titles()]
114+
]
77115

78116

79117
def download_href(path: str) -> str:
@@ -82,15 +120,22 @@ def download_href(path: str) -> str:
82120
Files below share/check_mk/agents are served statically by the Apache alias
83121
"check_mk/agents", so a relative URL resolves against the current page. Plugin
84122
family agent files live outside that tree (e.g. lib/python3/cmk/plugins/<family>/
85-
agents/) and are streamed through the GUI handler instead.
123+
agents/) and are streamed through a GUI handler instead - the one requiring
124+
authentication if the file belongs to a locally installed plugin family.
86125
"""
87126
agents_dir_prefix = str(cmk.utils.paths.agents_dir) + "/"
88127
if path.startswith(agents_dir_prefix):
89128
return "agents/%s" % path[len(agents_dir_prefix) :]
129+
130+
is_local = Path(path).is_relative_to(cmk.utils.paths.local_root)
90131
return makeuri_contextless(
91132
request,
92133
[("path", path)],
93-
filename=f"{DOWNLOAD_AGENT_PLUGIN_PAGE}.py",
134+
filename=(
135+
f"{DOWNLOAD_LOCAL_AGENT_PLUGIN_PAGE}.py"
136+
if is_local
137+
else f"{DOWNLOAD_AGENT_PLUGIN_PAGE}.py"
138+
),
94139
)
95140

96141

@@ -265,7 +310,7 @@ def _walk_base_dirs(self) -> list[str]:
265310
# * general information
266311
# * a (allow/deny) list of the files that should be exposed for download
267312
# * description / title for those.
268-
*(str(p) for p in _plugin_family_agent_dirs()),
313+
*(str(d.path) for d in _plugin_family_agent_dirs()),
269314
]
270315

271316
def _exclude_file_glob_patterns(self) -> list[str]:
@@ -280,7 +325,16 @@ def _exclude_file_glob_patterns(self) -> list[str]:
280325

281326
@cached_property
282327
def _title_map(self) -> Mapping[str, str]:
283-
return {str(p): t for p, t in _plugin_family_agent_titles()}
328+
return {
329+
str(d.path): (
330+
# Downloading these requires a login, while all other offered files
331+
# are served without authentication. Say so.
332+
_("%(family)s (locally installed, download requires login)") % {"family": d.title}
333+
if d.is_local
334+
else d.title
335+
)
336+
for d in _plugin_family_agent_dirs()
337+
}
284338

285339
def _title_for_root(self, root: str, relpath: str) -> str:
286340
# Files of a plugin family live in their own agents directory outside the
@@ -370,22 +424,36 @@ class PageDownloadAgentPlugin(Page):
370424
371425
Files grouped by plugin family (cmk/plugins/<family>/agents/) are not reachable
372426
through the "check_mk/agents" Apache alias, so ``ModeDownloadAgentsOther`` links
373-
them here. The requested path is validated against the set of plugin family agent
374-
directories before serving to prevent reading arbitrary files.
427+
them here. The requested path is validated against the passed set of plugin family
428+
agent directories before serving to prevent reading arbitrary files.
429+
430+
Authentication is decided before the page is called, so serving the files shipped
431+
with the version without a login (as the Apache alias does) and the files of locally
432+
installed plugin families with one requires two instances, registered under a
433+
"noauth:" and a regular page name respectively.
375434
"""
376435

436+
def __init__(
437+
self,
438+
allowed_dirs: Sequence[Path],
439+
*,
440+
require_permission: bool,
441+
) -> None:
442+
self.allowed_dirs: Final = [p.resolve() for p in allowed_dirs]
443+
self.require_permission: Final = require_permission
444+
377445
def page(self, ctx: PageContext) -> None:
378-
user.need_permission("wato.download_agents")
446+
if self.require_permission:
447+
user.need_permission("wato.download_agents")
379448

380449
try:
381450
requested = Path(ctx.request.get_str_input_mandatory("path")).resolve(strict=True)
382451
except (MKUserError, OSError):
383452
raise MKUserError("path", _("The requested file does not exist."))
384453

385-
allowed_dirs = [
386-
family_agents_dir.resolve() for family_agents_dir in _plugin_family_agent_dirs()
387-
]
388-
if not (requested.is_file() and any(requested.is_relative_to(d) for d in allowed_dirs)):
454+
if not (
455+
requested.is_file() and any(requested.is_relative_to(d) for d in self.allowed_dirs)
456+
):
389457
raise MKUserError("path", _("The requested file is not available for download."))
390458

391459
filename = requested.name

tests/unit/cmk/gui/test_pages.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def test_registered_pages() -> None:
8787
"edit_pagetype_topic",
8888
"dashboard",
8989
"download_agent_output",
90-
"download_agent_plugin",
90+
"download_local_agent_plugin",
9191
"download_crash_report",
9292
"download_diagnostics_dump",
9393
"edit_bookmark_list",
@@ -107,6 +107,7 @@ def test_registered_pages() -> None:
107107
"mobile",
108108
"mobile_view",
109109
"noauth:automation",
110+
"noauth:download_agent_plugin",
110111
"message",
111112
"prediction_graph",
112113
"parent_child_topology",

0 commit comments

Comments
 (0)