Skip to content

Commit 8bf0201

Browse files
authored
Merge pull request #126 from akquinet/regex-records-permission
allow records with regex
2 parents 83ed0b8 + c6e55d4 commit 8bf0201

File tree

5 files changed

+154
-8
lines changed

5 files changed

+154
-8
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,22 @@ environments:
104104
- "test.example.com"
105105
```
106106

107+
###### Regex
108+
109+
Additionally to the `records` list a `regex_records` list can be defined.
110+
In this list regex can be to define, which records are allowed.
111+
112+
```yaml
113+
...
114+
environments:
115+
- name: "Test1"
116+
...
117+
zones:
118+
- name: "example.com"
119+
regex_records:
120+
- "_acme-challenge.service-.*.example.com"
121+
```
122+
107123
##### Services
108124

109125
Under a `zone` `services` can be defined.

powerdns_api_proxy/config.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
RRSETRequest,
2020
ZoneNotAllowedException,
2121
)
22-
from powerdns_api_proxy.utils import check_zones_equal
22+
from powerdns_api_proxy.utils import check_record_in_regex, check_zones_equal
2323

2424

2525
@lru_cache(maxsize=1)
@@ -147,10 +147,18 @@ def check_rrset_allowed(zone: ProxyConfigZone, rrset: RRSET) -> bool:
147147
if zone.all_records:
148148
return True
149149

150+
if not rrset['name'].rstrip('.').endswith(zone.name.rstrip('.')):
151+
logger.debug('RRSET not allowed, because zone does not match')
152+
return False
153+
150154
for record in zone.records:
151155
if check_zones_equal(rrset['name'], record):
152156
return True
153157

158+
for regex in zone.regex_records:
159+
if check_record_in_regex(rrset['name'], regex):
160+
return True
161+
154162
if check_acme_record_allowed(zone, rrset):
155163
return True
156164

powerdns_api_proxy/models.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ class ProxyConfigServices(BaseModel):
1919
class ProxyConfigZone(BaseModel):
2020
'''
2121
`name` is the zone name.
22+
`description` is a description of the zone.
2223
`regex` should be set to `True` if `name` is a regex.
24+
`records` is a list of record names that are allowed.
25+
`regex_records` is a list of record regexes that are allowed.
2326
`admin` enabled creating and deleting the zone.
2427
`subzones` sets the same permissions on all subzones.
2528
`all_records` will be set to `True` if no `records` are defined.
@@ -30,6 +33,7 @@ class ProxyConfigZone(BaseModel):
3033
regex: bool = False
3134
description: str = ''
3235
records: list[str] = []
36+
regex_records: list[str] = []
3337
services: ProxyConfigServices = ProxyConfigServices(acme=False)
3438
admin: bool = False
3539
subzones: bool = False
@@ -38,7 +42,7 @@ class ProxyConfigZone(BaseModel):
3842

3943
def __init__(self, **data):
4044
super().__init__(**data)
41-
if len(self.records) == 0:
45+
if len(self.records) == 0 and len(self.regex_records) == 0:
4246
logger.debug(
4347
f'Setting all_records to True for zone {self.name}, because no records are defined'
4448
)

powerdns_api_proxy/utils.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ def check_zone_in_regex(zone: str, regex: str) -> bool:
2626
return re.match(regex, zone.rstrip('.')) is not None
2727

2828

29+
def check_record_in_regex(record: str, regex: str) -> bool:
30+
'''Checks if record is in regex'''
31+
return re.match(regex, record.rstrip('.')) is not None
32+
33+
2934
def check_zones_equal(zone1: str, zone2: str) -> bool:
3035
'''Checks if zones equal with or without trailing dot'''
3136
return zone1.rstrip('.') == zone2.rstrip('.')

tests/unit/config_test.py

Lines changed: 119 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -281,9 +281,9 @@ def test_check_rrset_not_allowed_single_entries():
281281
],
282282
)
283283
for item in [
284-
'entry1.test-zone.example.com.',
285-
'entry2.entry1.test-zone.example.com',
286-
'test-zone.example.com.',
284+
'entry100.test-zone.example.com.',
285+
'entry200.entry1.test-zone.example.com',
286+
'test-record.example.com.',
287287
]:
288288
rrset: RRSET = {
289289
'name': item,
@@ -293,7 +293,7 @@ def test_check_rrset_not_allowed_single_entries():
293293
'records': [],
294294
'comments': [],
295295
}
296-
assert check_rrset_allowed(zone, rrset)
296+
assert not check_rrset_allowed(zone, rrset)
297297

298298

299299
def test_check_rrsets_request_allowed_no_raise():
@@ -348,8 +348,8 @@ def test_check_rrsets_request_allowed_raise():
348348
)
349349
with pytest.raises(HTTPException) as err:
350350
ensure_rrsets_request_allowed(zone, request)
351-
assert err.value.status_code == 403
352-
assert err.value.detail == 'RRSET entry1.test-zone.example.com. not allowed'
351+
assert err.value.status_code == 403
352+
assert err.value.detail == 'RRSET entry1.test-zone.example.com. not allowed'
353353

354354

355355
def test_check_rrsets_request_not_allowed_read_only():
@@ -378,6 +378,119 @@ def test_check_rrsets_request_not_allowed_read_only():
378378
assert err.value.detail == 'RRSET update not allowed with read only token'
379379

380380

381+
def test_rrset_request_not_allowed_regex_empty():
382+
zone = ProxyConfigZone(
383+
name='test-zone.example.com.',
384+
regex_records=[],
385+
)
386+
request: RRSETRequest = {'rrsets': []}
387+
assert ensure_rrsets_request_allowed(zone, request)
388+
389+
390+
def test_rrset_request_allowed_all_regex():
391+
zone = ProxyConfigZone(
392+
name='test-zone.example.com.',
393+
regex_records=[
394+
'.*',
395+
],
396+
)
397+
request: RRSETRequest = {'rrsets': []}
398+
for item in [
399+
'entry1.test-zone.example.com.',
400+
'entry2.entry1.test-zone.example.com',
401+
]:
402+
request['rrsets'].append(
403+
{
404+
'name': item,
405+
'type': 'TXT',
406+
'changetype': 'REPLACE',
407+
'ttl': 3600,
408+
'records': [],
409+
'comments': [],
410+
}
411+
)
412+
assert ensure_rrsets_request_allowed(zone, request)
413+
414+
415+
def test_rrset_request_allowed_acme_regex():
416+
zone = ProxyConfigZone(
417+
name='test-zone.example.com.',
418+
regex_records=[
419+
'_acme-challenge.example.*.test-zone.example.com',
420+
],
421+
)
422+
request: RRSETRequest = {'rrsets': []}
423+
for item in [
424+
'_acme-challenge.example-entry.test-zone.example.com.',
425+
]:
426+
request['rrsets'].append(
427+
{
428+
'name': item,
429+
'type': 'TXT',
430+
'changetype': 'REPLACE',
431+
'ttl': 3600,
432+
'records': [],
433+
'comments': [],
434+
}
435+
)
436+
assert ensure_rrsets_request_allowed(zone, request)
437+
438+
439+
def test_rrset_request_not_allowed_false_regex():
440+
zone = ProxyConfigZone(
441+
name='test-zone.example.com.',
442+
regex_records=[
443+
'example.*.test-zone.example.com',
444+
],
445+
)
446+
request: RRSETRequest = {'rrsets': []}
447+
for item in [
448+
'entry1.test-zone.example.com.',
449+
'entry2.entry1.test-zone.example.com',
450+
]:
451+
request['rrsets'].append(
452+
{
453+
'name': item,
454+
'type': 'TXT',
455+
'changetype': 'REPLACE',
456+
'ttl': 3600,
457+
'records': [],
458+
'comments': [],
459+
}
460+
)
461+
with pytest.raises(HTTPException) as err:
462+
ensure_rrsets_request_allowed(zone, request)
463+
assert err.value.status_code == 403
464+
assert err.value.detail == 'RRSET entry1.test-zone.example.com. not allowed'
465+
466+
467+
def test_rrset_request_not_allowed_false_zone():
468+
zone = ProxyConfigZone(
469+
name='test-zone.example.com.',
470+
regex_records=[
471+
'example.*.test-zone2.example.com',
472+
],
473+
)
474+
request: RRSETRequest = {'rrsets': []}
475+
for item in [
476+
'example1.test-zone2.example.com.',
477+
]:
478+
request['rrsets'].append(
479+
{
480+
'name': item,
481+
'type': 'TXT',
482+
'changetype': 'REPLACE',
483+
'ttl': 3600,
484+
'records': [],
485+
'comments': [],
486+
}
487+
)
488+
with pytest.raises(HTTPException) as err:
489+
ensure_rrsets_request_allowed(zone, request)
490+
assert err.value.status_code == 403
491+
assert err.value.detail == 'RRSET example1.test-zone2.example.com. not allowed'
492+
493+
381494
def test_check_acme_record_allowed_all_records():
382495
zone = ProxyConfigZone(name='test-zone.example.com', all_records=True)
383496
rrset = RRSET(

0 commit comments

Comments
 (0)