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

@@ -247,8 +247,39 @@ ddns-service user passwd myuser
ddns-service user email myuser new@example.com ddns-service user email myuser new@example.com
``` ```
### Permission Management
Permissions control which hostnames users can update.
```bash
# List all permissions
ddns-service permission list
# List permissions for specific user
ddns-service permission list --user myuser
# Add permission for exact hostname
ddns-service permission add myuser mypc dyn.example.com
# Add wildcard permission (any hostname in zone)
ddns-service permission add myuser '*' dyn.example.com
# Add suffix wildcard (e.g., *.home matches foo.home, bar.home)
ddns-service permission add myuser '*.home' dyn.example.com
# Delete permission
ddns-service permission delete myuser mypc dyn.example.com
```
**Pattern matching:**
- `*` - matches any hostname in the zone
- `*.suffix` - matches hostnames ending in `.suffix` (recursive)
- `exact` - matches only that exact hostname
### Hostname Management ### Hostname Management
Hostnames are auto-created when users with permission send their first update.
```bash ```bash
# List all hostnames # List all hostnames
ddns-service hostname list ddns-service hostname list
@@ -256,13 +287,6 @@ ddns-service hostname list
# List hostnames for specific user # List hostnames for specific user
ddns-service hostname list --user myuser ddns-service hostname list --user myuser
# Add hostname
ddns-service hostname add myuser mypc dyn.example.com
# Add hostname with custom TTLs
ddns-service hostname add myuser mypc dyn.example.com \
--dns-ttl 60 --expiry-ttl 7200
# Modify hostname TTLs # Modify hostname TTLs
ddns-service hostname modify mypc dyn.example.com --dns-ttl 120 ddns-service hostname modify mypc dyn.example.com --dns-ttl 120

View File

@@ -96,10 +96,14 @@ def cleanup_expired(app, start_time=None):
continue continue
try: try:
if hostname.last_ipv4 is None and hostname.last_ipv6 is None:
hostname.delete_instance()
else:
hostname.save() hostname.save()
except DatabaseError as e: except DatabaseError as e:
logging.error( logging.error(
f"DB save failed after retries: hostname={hostname.hostname} " f"DB operation failed after retries: hostname={hostname.hostname} "
f"zone={hostname.zone}: {e}" f"zone={hostname.zone}: {e}"
) )

View File

@@ -12,6 +12,7 @@ from .models import (
get_hostname, get_hostname,
get_user, get_user,
Hostname, Hostname,
Permission,
User, User,
) )
@@ -166,58 +167,6 @@ def cmd_hostname_list(args, app):
return 0 return 0
def cmd_hostname_add(args, app):
"""Add a hostname."""
username = args.username
try:
# Get user
try:
user = get_user(username)
except DoesNotExist:
print(f"Error: User '{username}' not found.")
return 1
# Check if hostname+zone exists
try:
hostname = get_hostname(args.hostname, args.zone)
print(f"Error: Hostname '{hostname.hostname}' in zone '{hostname.zone}' exists.")
return 1
except EncodingError as e:
print(f"Error: {e}")
return 1
except DoesNotExist:
pass
# Get TTLs from args or config defaults
config = app.config
dns_ttl = args.dns_ttl
if dns_ttl is None:
dns_ttl = config["defaults"]["dns_ttl"]
expiry_ttl = args.expiry_ttl
if expiry_ttl is None:
expiry_ttl = config["defaults"]["expiry_ttl"]
# Create hostname
hostname = Hostname.create(
user=user,
hostname=args.hostname,
zone=args.zone,
dns_ttl=dns_ttl,
expiry_ttl=expiry_ttl
)
print(
f"Hostname '{hostname.hostname}' in zone '{hostname.zone}' added "
f"for user '{username}'."
)
except DatabaseError as e:
print(f"Database error: {e}")
return 1
return 0
def cmd_hostname_delete(args, app): def cmd_hostname_delete(args, app):
"""Delete a hostname.""" """Delete a hostname."""
try: try:
@@ -325,3 +274,101 @@ def cmd_cleanup(args, app):
return 1 return 1
return 0 return 0
def cmd_permission_list(args, app):
"""List permissions."""
query = Permission.select(Permission, User.username).join(User)
if args.user:
try:
user = get_user(args.user)
query = query.where(Permission.user == user)
except DoesNotExist:
print(f"Error: User '{args.user}' not found.")
return 1
query = query.order_by(
User.username, Permission.zone, Permission.hostname_pattern)
permissions = list(query)
if not permissions:
print("No permissions found.")
return 0
print(f"\n{'User':<20} {'Pattern':<30} {'Zone':<30}")
print("-" * 80)
for p in permissions:
print(f"{p.user.username:<20} {p.hostname_pattern:<30} {p.zone:<30}")
return 0
def cmd_permission_add(args, app):
"""Add a permission."""
username = args.username
pattern = args.hostname_pattern
zone = args.zone
try:
user = get_user(username)
except DoesNotExist:
print(f"Error: User '{username}' not found.")
return 1
# Validate pattern
if pattern != '*' and not pattern.startswith('*.') and '*' in pattern:
print("Error: Invalid pattern. Use '*', '*.suffix', or exact.")
return 1
# Check if permission exists
exists = Permission.select().where(
(Permission.user == user) &
(Permission.hostname_pattern == pattern) &
(Permission.zone == zone)
).exists()
if exists:
print("Error: Permission already exists.")
return 1
try:
Permission.create(
user=user,
hostname_pattern=pattern,
zone=zone
)
print(f"Permission added: {username} {pattern} {zone}")
except DatabaseError as e:
print(f"Database error: {e}")
return 1
return 0
def cmd_permission_delete(args, app):
"""Delete a permission."""
username = args.username
pattern = args.hostname_pattern
zone = args.zone
try:
user = get_user(username)
except DoesNotExist:
print(f"Error: User '{username}' not found.")
return 1
try:
perm = Permission.get(
(Permission.user == user) &
(Permission.hostname_pattern == pattern) &
(Permission.zone == zone)
)
perm.delete_instance()
print(f"Permission deleted: {username} {pattern} {zone}")
except DoesNotExist:
print("Error: Permission not found.")
return 1
except DatabaseError as e:
print(f"Database error: {e}")
return 1
return 0

View File

@@ -11,10 +11,12 @@ from . import __version__
from .app import Application from .app import Application
from .cli import ( from .cli import (
cmd_cleanup, cmd_cleanup,
cmd_hostname_add,
cmd_hostname_delete, cmd_hostname_delete,
cmd_hostname_list, cmd_hostname_list,
cmd_hostname_modify, cmd_hostname_modify,
cmd_permission_add,
cmd_permission_delete,
cmd_permission_list,
cmd_user_add, cmd_user_add,
cmd_user_delete, cmd_user_delete,
cmd_user_email, cmd_user_email,
@@ -81,14 +83,6 @@ def build_parser():
hostname_list.add_argument("--user", help="Filter by username") hostname_list.add_argument("--user", help="Filter by username")
hostname_list.set_defaults(func=cmd_hostname_list) hostname_list.set_defaults(func=cmd_hostname_list)
hostname_add = hostname_subparsers.add_parser("add", help="Add hostname")
hostname_add.add_argument("username", help="Username")
hostname_add.add_argument("hostname", help="Hostname (FQDN)")
hostname_add.add_argument("zone", help="DNS zone")
hostname_add.add_argument("--dns-ttl", type=int, help="DNS record TTL")
hostname_add.add_argument("--expiry-ttl", type=int, help="Expiry TTL")
hostname_add.set_defaults(func=cmd_hostname_add)
hostname_delete = hostname_subparsers.add_parser( hostname_delete = hostname_subparsers.add_parser(
"delete", help="Delete hostname" "delete", help="Delete hostname"
) )
@@ -105,6 +99,26 @@ def build_parser():
hostname_modify.add_argument("--expiry-ttl", type=int, help="Expiry TTL") hostname_modify.add_argument("--expiry-ttl", type=int, help="Expiry TTL")
hostname_modify.set_defaults(func=cmd_hostname_modify) hostname_modify.set_defaults(func=cmd_hostname_modify)
# Permission commands
perm_parser = subparsers.add_parser("permission", help="Permissions")
perm_subparsers = perm_parser.add_subparsers(dest="permission_command")
perm_list = perm_subparsers.add_parser("list", help="List permissions")
perm_list.add_argument("--user", help="Filter by username")
perm_list.set_defaults(func=cmd_permission_list)
perm_add = perm_subparsers.add_parser("add", help="Add permission")
perm_add.add_argument("username", help="Username")
perm_add.add_argument("hostname_pattern", help="Pattern (*, *.suffix, exact)")
perm_add.add_argument("zone", help="DNS zone")
perm_add.set_defaults(func=cmd_permission_add)
perm_delete = perm_subparsers.add_parser("delete", help="Delete permission")
perm_delete.add_argument("username", help="Username")
perm_delete.add_argument("hostname_pattern", help="Hostname pattern")
perm_delete.add_argument("zone", help="DNS zone")
perm_delete.set_defaults(func=cmd_permission_delete)
# Cleanup command # Cleanup command
cleanup_parser = subparsers.add_parser("cleanup", help="Run cleanup manually") cleanup_parser = subparsers.add_parser("cleanup", help="Run cleanup manually")
cleanup_parser.set_defaults(func=cmd_cleanup) cleanup_parser.set_defaults(func=cmd_cleanup)

View File

@@ -13,7 +13,6 @@ from peewee import (
Model, Model,
DateTimeField, DateTimeField,
DoesNotExist, DoesNotExist,
fn,
ForeignKeyField, ForeignKeyField,
IntegerField, IntegerField,
SqliteDatabase, SqliteDatabase,
@@ -29,12 +28,14 @@ __all__ = [
'DATABASE_VERSION', 'DATABASE_VERSION',
'User', 'User',
'Hostname', 'Hostname',
'Permission',
'Version', 'Version',
'init_database', 'init_database',
'create_tables', 'create_tables',
'get_user',
'get_hostname', 'get_hostname',
'get_hostname_for_user', 'get_hostname_for_user',
'get_permission',
'get_user',
'DoesNotExist', 'DoesNotExist',
'EncodingError', 'EncodingError',
'DatabaseError', 'DatabaseError',
@@ -44,10 +45,11 @@ __all__ = [
db = DatabaseProxy() db = DatabaseProxy()
# Current database schema version # Current database schema version
DATABASE_VERSION = 2 DATABASE_VERSION = 3
# Migration column mappings: key = target version # Migration column mappings: key = target version
# Values: {table: {old_col: new_col}} - None value = drop column # Values: {table: {old_col: new_col}} - None value = drop column
# Empty dict means no table schema changes (v3: new table only)
MIGRATION_COLUMN_MAPS = { MIGRATION_COLUMN_MAPS = {
2: { 2: {
'hostnames': { 'hostnames': {
@@ -62,7 +64,8 @@ MIGRATION_COLUMN_MAPS = {
'last_ipv6': 'last_ipv6', 'last_ipv6': 'last_ipv6',
'last_ipv6_update': 'last_ipv6_update', 'last_ipv6_update': 'last_ipv6_update',
} }
} },
3: {} # New permissions table, populated from hostnames
} }
@@ -129,6 +132,22 @@ class Hostname(BaseModel):
) )
class Permission(BaseModel):
"""Permission grants access to hostname patterns for users."""
id = AutoField()
user = ForeignKeyField(User, backref="permissions", on_delete="CASCADE")
hostname_pattern = CharField(max_length=255) # '*', '*.suffix', or exact
zone = CharField(max_length=255)
created_at = DateTimeFieldUTC(default=now_utc)
class Meta:
table_name = "permissions"
indexes = (
(('user', 'hostname_pattern', 'zone'), True),
)
class Version(BaseModel): class Version(BaseModel):
"""Database schema version for migrations.""" """Database schema version for migrations."""
@@ -142,6 +161,7 @@ class Version(BaseModel):
TABLE_TO_MODEL = { TABLE_TO_MODEL = {
'users': User, 'users': User,
'hostnames': Hostname, 'hostnames': Hostname,
'permissions': Permission,
} }
@@ -166,6 +186,7 @@ def init_database(config: dict):
actual_db = SqliteDatabase(db_path, pragmas={ actual_db = SqliteDatabase(db_path, pragmas={
'journal_mode': 'wal', 'journal_mode': 'wal',
'busy_timeout': 5000, 'busy_timeout': 5000,
'foreign_keys': 1,
}) })
db.initialize(actual_db) db.initialize(actual_db)
logging.debug(f"Database backend: SQLite path={db_path}") logging.debug(f"Database backend: SQLite path={db_path}")
@@ -238,6 +259,10 @@ def _migrate_table_sqlite(model_class, from_version: int, column_map: dict):
def _migrate_sqlite(from_version: int, to_version: int): def _migrate_sqlite(from_version: int, to_version: int):
"""Migrate SQLite from from_version to to_version.""" """Migrate SQLite from from_version to to_version."""
if to_version == 3:
_migrate_v3_create_permissions()
return
db.execute_sql('PRAGMA foreign_keys=OFF') db.execute_sql('PRAGMA foreign_keys=OFF')
try: try:
tables = MIGRATION_COLUMN_MAPS[to_version] tables = MIGRATION_COLUMN_MAPS[to_version]
@@ -248,6 +273,16 @@ def _migrate_sqlite(from_version: int, to_version: int):
db.execute_sql('PRAGMA foreign_keys=ON') db.execute_sql('PRAGMA foreign_keys=ON')
def _migrate_v3_create_permissions():
"""Create permissions table and populate from existing hostnames."""
db.create_tables([Permission])
db.execute_sql(
'INSERT INTO permissions '
'(user_id, hostname_pattern, zone, created_at) '
'SELECT user_id, hostname, zone, CURRENT_TIMESTAMP FROM hostnames'
)
def _migrate_mariadb(to_version: int): def _migrate_mariadb(to_version: int):
"""Migrate MariaDB to target version using ALTER TABLE.""" """Migrate MariaDB to target version using ALTER TABLE."""
if to_version == 2: if to_version == 2:
@@ -256,6 +291,8 @@ def _migrate_mariadb(to_version: int):
'ALTER TABLE hostnames ADD UNIQUE INDEX ' 'ALTER TABLE hostnames ADD UNIQUE INDEX '
'hostnames_hostname_zone (hostname, zone)' 'hostnames_hostname_zone (hostname, zone)'
) )
elif to_version == 3:
_migrate_v3_create_permissions()
def check_and_migrate(): def check_and_migrate():
@@ -290,7 +327,7 @@ def create_tables():
check_and_migrate() check_and_migrate()
return return
db.create_tables([User, Hostname, Version]) db.create_tables([User, Hostname, Permission, Version])
Version.create(version=DATABASE_VERSION) Version.create(version=DATABASE_VERSION)
logging.debug("Database tables created") logging.debug("Database tables created")
@@ -316,6 +353,52 @@ def get_user(username: str) -> User:
return User.get(User.username == username) return User.get(User.username == username)
def get_permission(user, fqdn):
"""
Get permission for user to access FQDN.
Patterns:
'*' - matches any hostname
'*.suffix' - matches hostname ends with .suffix
'exact' - matches hostname == pattern
Args:
user: User to check permission for.
fqdn: Full qualified domain name.
Returns:
Permission instance.
Raises:
DoesNotExist: If permission not found.
EncodingError: If fqdn is invalid.
"""
fqdn = encode_dnsname(fqdn)
permissions = Permission.select().where(Permission.user == user)
for perm in permissions:
if not fqdn.endswith(f".{perm.zone}"):
continue
pattern = perm.hostname_pattern
if pattern == '*':
return perm
hostname = fqdn.removesuffix(f".{perm.zone}")
if pattern.startswith('*.'):
suffix = pattern[1:] # Remove '*'
if hostname.endswith(suffix):
return perm
elif hostname == pattern:
return perm
raise DoesNotExist
def get_hostname(hostname: str, zone: str) -> Hostname: def get_hostname(hostname: str, zone: str) -> Hostname:
""" """
Get hostname by name and zone. Get hostname by name and zone.
@@ -342,27 +425,44 @@ def get_hostname(hostname: str, zone: str) -> Hostname:
) )
def get_hostname_for_user(hostname: str, user: User): def get_hostname_for_user(
user: User, hostname: str, zone: str, dns_ttl: int, expiry_ttl: int):
""" """
Get hostname owned by specific user. Get hostname if it exists or create a new intance.
Args: Args:
hostname: Hostname to look up (FQDN). user: User requesting access.
user: User who should own the hostname. hostname: Hostname (e.g., 'myhost').
zone: Zone (e.g., 'example.com').
dns_ttl: Expiry TTL for auto-created hostnames.
expiry_ttl: Expiry TTL for auto-created hostnames.
Returns: Returns:
Hostname instance. Tuple of (Hostname, created) where created is True if auto-created.
Raises:
DoesNotExist: If hostname not found or not owned by user.
EncodingError: If hostname is invalid.
Example: Example:
>>> user = get_user("alice") >>> host, created = get_hostname_or_create(
>>> host = get_hostname_for_user("myhost.example.com", user) ... user, 'myhost', 'example.com', 60, 3600)
""" """
fqdn = fn.Concat(Hostname.hostname, '.', Hostname.zone) try:
return Hostname.get( return (
(fqdn == encode_dnsname(hostname)) & Hostname.get(
(Hostname.user == user) (Hostname.user == user) &
(Hostname.hostname == hostname) &
(Hostname.zone == zone)
),
False
)
except DoesNotExist:
pass
return (
Hostname(
user=user,
hostname=hostname,
zone=zone,
dns_ttl=dns_ttl,
expiry_ttl=expiry_ttl,
),
True
) )

View File

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