Add and use permission table, update CLI and README.md

This commit is contained in:
2026-02-08 02:42:49 +01:00
parent eafe106bf1
commit 81ffdc9925
6 changed files with 337 additions and 141 deletions

View File

@@ -30,7 +30,8 @@ from .models import (
DoesNotExist,
EncodingError,
get_hostname_for_user,
get_user
get_user,
get_permission,
)
from argon2.exceptions import VerifyMismatchError
from concurrent.futures import ThreadPoolExecutor
@@ -294,7 +295,7 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
# Parse query parameters
params = parse_qs(parsed.query)
# Get credentials
# Process credentials parameters
username, password = self.parse_basic_auth()
if username is None:
username = extract_param(params, endpoint["params"]["username"])
@@ -308,7 +309,7 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
client_ip
)
# Get hostname parameter
# Process hostname parameter
hostname_param = extract_param(params, endpoint["params"]["hostname"])
if not hostname_param:
raise DDNSClientError(
@@ -319,12 +320,6 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
username=username
)
# Validate credentials
user = self._authenticate(client_ip, username, password)
# Check hostname ownership
hostname = self._check_permissions(client_ip, user, hostname_param)
# Process myip parameter
ipv4 = None
myip = extract_param(params, endpoint["params"]["ipv4"])
@@ -342,8 +337,7 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
STATUS_BADIP,
client=client_ip,
username=username,
hostname=hostname.hostname,
zone=hostname.zone,
hostname=hostname_param,
ip=myip
)
@@ -359,13 +353,12 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
raise ValueError
except ValueError:
raise DDNSClientError(
"Bad IP address",
"Bad IPv6 address",
400,
STATUS_BADIP,
client=client_ip,
username=username,
hostname=hostname.hostname,
zone=hostname.zone,
hostname=hostname_param,
ipv6=myip6
)
@@ -377,6 +370,13 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
else:
ipv6 = ip
# Process notify_change parameter
notify_change = extract_param(
params, endpoint["params"]["notify_change"])
notify_change = (notify_change.lower() in
["1", "y", "yes", "on", "true"]
if notify_change else False)
# Process expiry_ttl parameter
expiry_ttl_param = extract_param(
params, endpoint["params"]["expiry_ttl"])
@@ -393,30 +393,26 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
STATUS_NOHOST,
client=client_ip,
username=username,
hostname=hostname.hostname,
zone=hostname.zone,
hostname=hostname_param,
expiry_ttl=expiry_ttl_param
)
# Validate bounds
defaults = self.app.config["defaults"]
allow_zero = defaults.get("expiry_ttl_allow_zero", True)
ttl_min = defaults.get("expiry_ttl_min")
ttl_max = defaults.get("expiry_ttl_max")
if expiry_ttl == 0:
if not allow_zero:
if not defaults["expiry_ttl_allow_zero"]:
raise DDNSClientError(
"Zero expiry_ttl not allowed",
400,
STATUS_NOHOST,
client=client_ip,
username=username,
hostname=hostname.hostname,
zone=hostname.zone,
hostname=hostname_param,
expiry_ttl=expiry_ttl
)
else:
ttl_min = defaults["expiry_ttl_min"]
if ttl_min is not None and expiry_ttl < ttl_min:
raise DDNSClientError(
"expiry_ttl below minimum",
@@ -424,11 +420,11 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
STATUS_NOHOST,
client=client_ip,
username=username,
hostname=hostname.hostname,
zone=hostname.zone,
hostname=hostname_param,
expiry_ttl=expiry_ttl,
min=ttl_min
)
ttl_max = defaults["expiry_ttl_max"]
if ttl_max is not None and expiry_ttl > ttl_max:
raise DDNSClientError(
"expiry_ttl above maximum",
@@ -436,18 +432,20 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
STATUS_NOHOST,
client=client_ip,
username=username,
hostname=hostname.hostname,
zone=hostname.zone,
hostname=hostname_param,
expiry_ttl=expiry_ttl,
max=ttl_max
)
# Process notify_change parameter
notify_change = extract_param(
params, endpoint["params"]["notify_change"])
notify_change = (notify_change.lower() in
["1", "y", "yes", "on", "true"]
if notify_change else False)
# Validate credentials
user = self._authenticate(client_ip, username, password)
# Check hostname permission
hostname, created = self._get_hostname_for_user(
client_ip,
user,
hostname_param
)
# Good rate limit check
if self.app.good_limiter:
@@ -474,7 +472,8 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
ipv4,
ipv6,
notify_change,
expiry_ttl
expiry_ttl,
created
)
def _authenticate(self, client_ip, username, password):
@@ -498,28 +497,33 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
return user
def _check_permissions(self, client_ip, user, hostname_param):
# Check hostname ownership
def _get_hostname_for_user(self, client_ip, user, hostname_param):
"""Check permissions and get/create hostname."""
code = None
try:
hostname = get_hostname_for_user(hostname_param, user)
perm = get_permission(user, hostname_param)
hostname_param = hostname_param.removesuffix(f".{perm.zone}")
return get_hostname_for_user(
user,
hostname_param,
perm.zone,
self.app.config["defaults"]["dns_ttl"],
self.app.config["defaults"]["expiry_ttl"]
)
except DoesNotExist:
code = 403
except EncodingError:
code = 400
if code:
raise DDNSClientError(
"Access denied",
code,
STATUS_NOHOST,
client=client_ip,
username=user.username,
hostname=hostname_param
)
return hostname
raise DDNSClientError(
"Access denied",
code,
STATUS_NOHOST,
client=client_ip,
username=user.username,
hostname=hostname_param
)
def _rollback_dns(self, hostname, old_ip, record_type):
"""Roll back a DNS record to its previous value."""
@@ -535,7 +539,7 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
logging.error(f"DNS rollback failed ({record_type}): {e}")
def _process_ip_update(self, client_ip, user, hostname, ipv4, ipv6,
notify_change, expiry_ttl):
notify_change, expiry_ttl, created):
"""Process IP update for hostname."""
now = now_utc()
@@ -631,10 +635,12 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
STATUS_NOCHG,
ipv4=hostname.last_ipv4,
ipv6=hostname.last_ipv6,
expiry_ttl=hostname.expiry_ttl
expiry_ttl=hostname.expiry_ttl,
created=created
)
return
action = "Created" if created else "Updated"
changed_info = ""
if ipv4_changed:
changed_info += f" ipv4={ipv4}"
@@ -643,7 +649,7 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
if expiry_ttl_changed:
changed_info += f" expiry_ttl={hostname.expiry_ttl}"
logging.info(
f"Updated: client={client_ip} hostname={hostname.hostname} "
f"{action}: client={client_ip} hostname={hostname.hostname} "
f"zone={hostname.zone}{changed_info} notify_change={str(notify_change).lower()}"
)
@@ -663,7 +669,8 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
STATUS_GOOD,
ipv4=hostname.last_ipv4,
ipv6=hostname.last_ipv6,
expiry_ttl=hostname.expiry_ttl
expiry_ttl=hostname.expiry_ttl,
created=created
)