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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user