feat: scoped host merge for non-admin config saves

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NfPpSpccTWBfZg1FTveyaU
This commit is contained in:
2026-07-09 16:43:08 -04:00
co-authored by Claude Fable 5
parent 98b50dbbf4
commit 5c6f462081
2 changed files with 214 additions and 0 deletions
+80
View File
@@ -59,3 +59,83 @@ def user_threshold_configs(threshold_cfgs: Any, username: str) -> Dict[str, Any]
name: cfg for name, cfg in (threshold_cfgs or {}).items()
if isinstance(cfg, dict) and user_can_use(cfg, username)
}
_HOST_SETTING_KEYS = ("watch", "dyndns", "notification_channels", "threshold_config")
_HOST_ACCESS_KEYS = ("owner", "managers", "monitors")
_HOST_ALLOWED_KEYS = frozenset(_HOST_SETTING_KEYS) | frozenset(_HOST_ACCESS_KEYS)
def _check_added_assignments(host_name: str, old_cfg: Any, new_cfg: dict,
key: str, usable: set) -> None:
"""Every value under *key* not already on the host must be in *usable*."""
old_vals = set(_as_list((old_cfg or {}).get(key)))
for v in _as_list(new_cfg.get(key)):
if v not in old_vals and v not in usable:
raise ScopedMergeError(
f"host {host_name!r}: {key} entry {v!r} is not available to you")
def merge_hosts_scoped(existing: Any, payload: Any, username: str,
channels_cfg: Any, threshold_cfgs: Any) -> Dict[str, Any]:
"""Return a new hosts section with *username*'s visible subset replaced by *payload*.
Hosts the user cannot see are preserved untouched. Visible hosts missing
from the payload are deleted (owners only). Managers may change setting
keys but not access keys and may not delete. New hosts get their owner
forced to *username*. Newly added channel/threshold assignments must be
global or owned by the user. Violations raise ScopedMergeError.
"""
existing = existing or {}
payload = payload or {}
visible = user_hosts(existing, username)
usable_channels = set(user_channels(channels_cfg, username))
usable_tcs = set(user_threshold_configs(threshold_cfgs, username)) | {"default"}
result: Dict[str, Any] = {n: c for n, c in existing.items() if n not in visible}
for name, entry in payload.items():
if not isinstance(entry, dict):
raise ScopedMergeError(f"host {name!r}: invalid entry")
unknown = set(entry) - _HOST_ALLOWED_KEYS
if unknown:
raise ScopedMergeError(
f"host {name!r}: fields not permitted: {', '.join(sorted(unknown))}")
if name in existing and name not in visible:
raise ScopedMergeError(f"host {name!r}: you are not an owner or manager")
old = visible.get(name)
if old is None:
new_cfg = dict(entry)
new_cfg["owner"] = username
else:
new_cfg = dict(old)
for key in _HOST_SETTING_KEYS:
if key in entry:
new_cfg[key] = entry[key]
else:
new_cfg.pop(key, None)
if old.get("owner") == username:
for key in _HOST_ACCESS_KEYS:
if key in entry:
new_cfg[key] = entry[key]
else:
new_cfg.pop(key, None)
else:
if "owner" in entry and (entry.get("owner") or None) != (old.get("owner") or None):
raise ScopedMergeError(
f"host {name!r}: only the owner may change ownership")
for key in ("managers", "monitors"):
if key in entry and set(_as_list(entry[key])) != set(_as_list(old.get(key))):
raise ScopedMergeError(
f"host {name!r}: only the owner may change {key}")
_check_added_assignments(name, old, new_cfg, "notification_channels", usable_channels)
_check_added_assignments(name, old, new_cfg, "threshold_config", usable_tcs)
result[name] = new_cfg
for name, old in visible.items():
if name not in payload and old.get("owner") != username:
raise ScopedMergeError(f"host {name!r}: only the owner may delete a host")
return result