Skip to content

Commit 1cf7f02

Browse files
authored
Merge pull request #175 from akquinet/copilot/fix-174
Fix: Allow `global_read_only` without requiring `zones` field and preserve explicit zone permissions
2 parents eb75f0b + ae835d6 commit 1cf7f02

File tree

2 files changed

+90
-11
lines changed

2 files changed

+90
-11
lines changed

powerdns_api_proxy/models.py

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from functools import lru_cache
22
from typing import TypedDict
33

4-
from pydantic import BaseModel, field_validator
4+
from pydantic import BaseModel, field_validator, model_validator
55

66
from powerdns_api_proxy.logging import logger
77
from powerdns_api_proxy.utils import (
@@ -26,7 +26,7 @@ class ProxyConfigZone(BaseModel):
2626
`admin` enabled creating and deleting the zone.
2727
`subzones` sets the same permissions on all subzones.
2828
`all_records` will be set to `True` if no `records` are defined.
29-
`read_only` will be set to `True` if `global_read_only` is `True`.
29+
`read_only` controls write permissions for this specific zone.
3030
"""
3131

3232
name: str
@@ -53,7 +53,7 @@ def __init__(self, **data):
5353
class ProxyConfigEnvironment(BaseModel):
5454
name: str
5555
token_sha512: str
56-
zones: list[ProxyConfigZone]
56+
zones: list[ProxyConfigZone] = []
5757
global_read_only: bool = False
5858
global_search: bool = False
5959
global_tsigkeys: bool = False
@@ -74,17 +74,20 @@ def validate_token(cls, token_sha512):
7474
raise ValueError("A SHA512 hash must be 128 digits long")
7575
return token_sha512
7676

77+
@model_validator(mode="after")
78+
def validate_zones_or_global_read_only(self):
79+
if not self.zones and not self.global_read_only:
80+
raise ValueError(
81+
"Either 'zones' must be non-empty or 'global_read_only' must be True"
82+
)
83+
return self
84+
7785
def __init__(self, **data):
7886
super().__init__(**data)
79-
if self.global_read_only:
80-
logger.debug(
81-
"Setting all subzones to read_only, because global_read_only is true"
82-
)
83-
for zone in self.zones:
84-
zone.read_only = True
8587

86-
# populate zones lookup
87-
self._zones_lookup[zone.name] = zone
88+
# populate zones lookup
89+
for zone in self.zones:
90+
self._zones_lookup[zone.name] = zone
8891

8992
def __hash__(self):
9093
return hash(

tests/unit/config_test.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,3 +595,79 @@ def test_tsigkeys_allowed_globally():
595595
environment = deepcopy(dummy_proxy_environment)
596596
environment.global_tsigkeys = True
597597
assert check_pdns_tsigkeys_allowed(environment) is True
598+
599+
600+
def test_global_read_only_without_zones():
601+
"""Test that global_read_only=True allows empty zones list"""
602+
env = ProxyConfigEnvironment(
603+
name="Test Global Read Only",
604+
token_sha512=dummy_proxy_environment_token_sha512,
605+
global_read_only=True,
606+
)
607+
assert env.global_read_only is True
608+
assert env.zones == []
609+
610+
611+
def test_environment_with_neither_zones_nor_global_read_only_fails():
612+
"""Test that providing neither zones nor global_read_only fails validation"""
613+
with pytest.raises(ValueError) as err:
614+
ProxyConfigEnvironment(
615+
name="test", token_sha512=dummy_proxy_environment_token_sha512
616+
)
617+
assert "Either 'zones' must be non-empty or 'global_read_only' must be True" in str(
618+
err.value
619+
)
620+
621+
622+
def test_environment_with_empty_zones_and_no_global_read_only_fails():
623+
"""Test that explicitly providing empty zones without global_read_only fails"""
624+
with pytest.raises(ValueError) as err:
625+
ProxyConfigEnvironment(
626+
name="test", token_sha512=dummy_proxy_environment_token_sha512, zones=[]
627+
)
628+
assert "Either 'zones' must be non-empty or 'global_read_only' must be True" in str(
629+
err.value
630+
)
631+
632+
633+
def test_proxy_config_with_global_read_only_environment():
634+
"""Test that ProxyConfig works with global_read_only environment without zones"""
635+
config = ProxyConfig(
636+
pdns_api_url="https://powerdns-api.example.com",
637+
pdns_api_token="blablub",
638+
environments=[
639+
ProxyConfigEnvironment(
640+
name="foo",
641+
token_sha512=dummy_proxy_environment_token_sha512,
642+
global_read_only=True,
643+
)
644+
],
645+
)
646+
assert config.environments[0].global_read_only is True
647+
assert config.environments[0].zones == []
648+
649+
650+
def test_global_read_only_with_explicit_zones_keeps_zone_permissions():
651+
"""Test that global_read_only=True doesn't force explicit zones to be read_only"""
652+
# Create a zone that should remain writable
653+
writable_zone = ProxyConfigZone(name="example.com", read_only=False)
654+
readonly_zone = ProxyConfigZone(name="readonly.com", read_only=True)
655+
656+
env = ProxyConfigEnvironment(
657+
name="Test Global Read Only with Zones",
658+
token_sha512=dummy_proxy_environment_token_sha512,
659+
zones=[writable_zone, readonly_zone],
660+
global_read_only=True,
661+
)
662+
663+
# global_read_only should be True
664+
assert env.global_read_only is True
665+
666+
# But explicit zones should keep their original read_only settings
667+
assert env.zones[0].read_only is False # writable_zone should remain writable
668+
assert env.zones[1].read_only is True # readonly_zone should remain read_only
669+
670+
# Should have access to zones via lookup
671+
assert len(env._zones_lookup) == 2
672+
assert "example.com" in env._zones_lookup
673+
assert "readonly.com" in env._zones_lookup

0 commit comments

Comments
 (0)