Skip to content

Commit 43d8933

Browse files
authored
Merge branch 'main' into cryptokeys
2 parents 536e329 + 1cf7f02 commit 43d8933

File tree

3 files changed

+94
-14
lines changed

3 files changed

+94
-14
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,8 +26,8 @@ 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`.
3029
`cryptokeys` enables management of DNSSEC.
30+
`read_only` controls write permissions for this specific zone.
3131
"""
3232

3333
name: str
@@ -55,7 +55,7 @@ def __init__(self, **data):
5555
class ProxyConfigEnvironment(BaseModel):
5656
name: str
5757
token_sha512: str
58-
zones: list[ProxyConfigZone]
58+
zones: list[ProxyConfigZone] = []
5959
global_read_only: bool = False
6060
global_search: bool = False
6161
global_cryptokeys: bool = False
@@ -77,17 +77,20 @@ def validate_token(cls, token_sha512):
7777
raise ValueError("A SHA512 hash must be 128 digits long")
7878
return token_sha512
7979

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

89-
# populate zones lookup
90-
self._zones_lookup[zone.name] = zone
91+
# populate zones lookup
92+
for zone in self.zones:
93+
self._zones_lookup[zone.name] = zone
9194

9295
def __hash__(self):
9396
return hash(

powerdns_api_proxy/proxy.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,9 @@
33
from http import HTTPStatus
44
from typing import Literal
55

6-
import sentry_sdk
76
from fastapi import APIRouter, Depends, FastAPI, Header, Request, Response
87
from fastapi.responses import HTMLResponse, JSONResponse
98
from prometheus_fastapi_instrumentator import Instrumentator, metrics
10-
from sentry_sdk.integrations.aiohttp import AioHttpIntegration
11-
from sentry_sdk.integrations.fastapi import FastApiIntegration
129
from starlette.exceptions import HTTPException as StarletteHTTPException
1310

1411
from powerdns_api_proxy.config import (
@@ -40,6 +37,10 @@
4037
from powerdns_api_proxy.pdns import PDNSConnector, handle_pdns_response
4138

4239
if os.getenv("SENTRY_DSN"):
40+
import sentry_sdk
41+
from sentry_sdk.integrations.aiohttp import AioHttpIntegration
42+
from sentry_sdk.integrations.fastapi import FastApiIntegration
43+
4344
sentry_sdk.init(
4445
traces_sample_rate=float(os.getenv("SENTRY_TRACES_SAMPLE_RATE") or 0.1),
4546
environment=os.getenv("ENVIRONMENT") or "DEV",

tests/unit/config_test.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,3 +616,79 @@ def test_tsigkeys_allowed_globally():
616616
environment = deepcopy(dummy_proxy_environment)
617617
environment.global_tsigkeys = True
618618
assert check_pdns_tsigkeys_allowed(environment) is True
619+
620+
621+
def test_global_read_only_without_zones():
622+
"""Test that global_read_only=True allows empty zones list"""
623+
env = ProxyConfigEnvironment(
624+
name="Test Global Read Only",
625+
token_sha512=dummy_proxy_environment_token_sha512,
626+
global_read_only=True,
627+
)
628+
assert env.global_read_only is True
629+
assert env.zones == []
630+
631+
632+
def test_environment_with_neither_zones_nor_global_read_only_fails():
633+
"""Test that providing neither zones nor global_read_only fails validation"""
634+
with pytest.raises(ValueError) as err:
635+
ProxyConfigEnvironment(
636+
name="test", token_sha512=dummy_proxy_environment_token_sha512
637+
)
638+
assert "Either 'zones' must be non-empty or 'global_read_only' must be True" in str(
639+
err.value
640+
)
641+
642+
643+
def test_environment_with_empty_zones_and_no_global_read_only_fails():
644+
"""Test that explicitly providing empty zones without global_read_only fails"""
645+
with pytest.raises(ValueError) as err:
646+
ProxyConfigEnvironment(
647+
name="test", token_sha512=dummy_proxy_environment_token_sha512, zones=[]
648+
)
649+
assert "Either 'zones' must be non-empty or 'global_read_only' must be True" in str(
650+
err.value
651+
)
652+
653+
654+
def test_proxy_config_with_global_read_only_environment():
655+
"""Test that ProxyConfig works with global_read_only environment without zones"""
656+
config = ProxyConfig(
657+
pdns_api_url="https://powerdns-api.example.com",
658+
pdns_api_token="blablub",
659+
environments=[
660+
ProxyConfigEnvironment(
661+
name="foo",
662+
token_sha512=dummy_proxy_environment_token_sha512,
663+
global_read_only=True,
664+
)
665+
],
666+
)
667+
assert config.environments[0].global_read_only is True
668+
assert config.environments[0].zones == []
669+
670+
671+
def test_global_read_only_with_explicit_zones_keeps_zone_permissions():
672+
"""Test that global_read_only=True doesn't force explicit zones to be read_only"""
673+
# Create a zone that should remain writable
674+
writable_zone = ProxyConfigZone(name="example.com", read_only=False)
675+
readonly_zone = ProxyConfigZone(name="readonly.com", read_only=True)
676+
677+
env = ProxyConfigEnvironment(
678+
name="Test Global Read Only with Zones",
679+
token_sha512=dummy_proxy_environment_token_sha512,
680+
zones=[writable_zone, readonly_zone],
681+
global_read_only=True,
682+
)
683+
684+
# global_read_only should be True
685+
assert env.global_read_only is True
686+
687+
# But explicit zones should keep their original read_only settings
688+
assert env.zones[0].read_only is False # writable_zone should remain writable
689+
assert env.zones[1].read_only is True # readonly_zone should remain read_only
690+
691+
# Should have access to zones via lookup
692+
assert len(env._zones_lookup) == 2
693+
assert "example.com" in env._zones_lookup
694+
assert "readonly.com" in env._zones_lookup

0 commit comments

Comments
 (0)