From aaf12e1b03cf0ce0b8ed13470bcdeb5504b332a3 Mon Sep 17 00:00:00 2001 From: Guillaume-Jean Herbiet Date: Thu, 3 Oct 2024 14:55:36 +0200 Subject: [PATCH 01/12] Attach existing catalog zone when creating a new zone. --- migrations/007.php | 7 +++++++ model/migrationdirectory.php | 2 +- model/zone.php | 8 ++++++++ model/zonedirectory.php | 23 +++++++++++++++++++++-- templates/zone.php | 16 ++++++++++++++++ templates/zones.php | 12 ++++++++++++ views/zone.php | 3 +++ views/zones.php | 3 +++ 8 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 migrations/007.php diff --git a/migrations/007.php b/migrations/007.php new file mode 100644 index 0000000..f047c6b --- /dev/null +++ b/migrations/007.php @@ -0,0 +1,7 @@ +database->exec('ALTER TABLE ONLY zone ADD catalog text;'); + +$this->database->exec("INSERT INTO replication_type VALUES (3, 'Producer', 'A producer or catalog zone is a special zone that store the current list of zones assoicated with it. It can be used by secondary servers to update the list of zones for which they are authoritative')"); + diff --git a/model/migrationdirectory.php b/model/migrationdirectory.php index 4d49451..83de45d 100644 --- a/model/migrationdirectory.php +++ b/model/migrationdirectory.php @@ -22,7 +22,7 @@ class MigrationDirectory extends DBDirectory { /** * Increment this constant to activate a new migration from the migrations directory */ - const LAST_MIGRATION = 6; + const LAST_MIGRATION = 7; public function __construct() { parent::__construct(); diff --git a/model/zone.php b/model/zone.php index da23521..7874ec5 100644 --- a/model/zone.php +++ b/model/zone.php @@ -48,6 +48,10 @@ class Zone extends Record { */ private $api_rectify = null; /** + * Consumer catalog zone for this zone + */ + private $catalog = null; + /** * List of changes to be applied to the zone when doing ->commit_changes() */ private $changes = array(); @@ -92,6 +96,8 @@ public function __set($field, $value) { case 'api_rectify': $this->api_rectify = $value; break; + case 'catalog': + $this->catalog = $value; default: parent::__set($field, $value); } @@ -201,6 +207,7 @@ public function update() { global $config; $update = new StdClass; $update->kind = $this->kind; + $update->catalog = $this->catalog; $update->account = $this->account; if(isset($config['dns']['dnssec']) && $config['dns']['dnssec'] == 1) { $update->dnssec = (bool)$this->dnssec; @@ -648,6 +655,7 @@ public function restore() { $data = new StdClass; $data->name = $this->name; $data->kind = $this->kind; + $data->catalog = $this->catalog; $data->nameservers = array(); $data->rrsets = array(); foreach($rrsets as $rrset) { diff --git a/model/zonedirectory.php b/model/zonedirectory.php index b4ea1d1..dcdf690 100644 --- a/model/zonedirectory.php +++ b/model/zonedirectory.php @@ -39,13 +39,14 @@ public function __construct() { * @param Zone $zone to be added */ public function add_zone(Zone $zone) { - $stmt = $this->database->prepare('INSERT INTO zone (pdns_id, name, serial, kind, account, dnssec) VALUES (?, ?, ?, ?, ?, ?)'); + $stmt = $this->database->prepare('INSERT INTO zone (pdns_id, name, serial, kind, account, dnssec, catalog) VALUES (?, ?, ?, ?, ?, ?, ?)'); $stmt->bindParam(1, $zone->pdns_id, PDO::PARAM_STR); $stmt->bindParam(2, $zone->name, PDO::PARAM_STR); $stmt->bindParam(3, $zone->serial, PDO::PARAM_INT); $stmt->bindParam(4, $zone->kind, PDO::PARAM_STR); $stmt->bindParam(5, $zone->account, PDO::PARAM_STR); $stmt->bindParam(6, $zone->dnssec, PDO::PARAM_INT); + $stmt->bindParam(7, $zone->catalog, PDO::PARAM_STR); try { $stmt->execute(); $zone->id = $this->database->lastInsertId('zone_id_seq'); @@ -73,6 +74,7 @@ public function create_zone($zone) { $data = new StdClass; $data->name = $zone->name; $data->kind = $zone->kind; + $data->catalog = $zone->catalog; $data->nameservers = $zone->nameservers; $data->rrsets = array(); foreach($zone->list_resource_record_sets() as $rrset) { @@ -149,6 +151,7 @@ public function list_zones($include = array()) { $zone->pdns_id = $pdns_zone->id; $zone->name = $pdns_zone->name; $zone->kind = $pdns_zone->kind; + $zone->catalog = $pdns_zone->catalog; $zone->serial = $pdns_zone->serial; $zone->account = $pdns_zone->account; $zone->dnssec = $pdns_zone->dnssec; @@ -156,7 +159,7 @@ public function list_zones($include = array()) { $zones_by_pdns_id[$zone->pdns_id] = $zone; $current_zones[$zone->pdns_id] = true; } else { - $fields = array('serial' => PDO::PARAM_INT, 'kind' => PDO::PARAM_STR, 'account' => PDO::PARAM_STR, 'dnssec' => PDO::PARAM_INT); + $fields = array('serial' => PDO::PARAM_INT, 'kind' => PDO::PARAM_STR, 'account' => PDO::PARAM_STR, 'dnssec' => PDO::PARAM_INT, 'catalog' => PDO::PARAM_STR); foreach($fields as $field => $type) { if($zones_by_pdns_id[$pdns_zone->id]->{$field} != $pdns_zone->{$field}) { $zones_by_pdns_id[$pdns_zone->id]->{$field} = $pdns_zone->{$field}; @@ -191,6 +194,22 @@ public function list_zones($include = array()) { return $zones_by_pdns_id; } + /** + * Fetch the list of zones matching the specific type + * @param string $type of the zones to list + * @return array of Zone objects indexed by pdns_id + */ + public function list_zones_by_kind($type) { + $stmt = $this->database->prepare('SELECT * FROM zone WHERE kind = ?'); + $stmt->bindParam(1, $type, PDO::PARAM_STR); + $stmt->execute(); + $zones_by_pdns_id = array(); + while($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + $zones_by_pdns_id[$row['pdns_id']] = new Zone($row['id'], $row); + } + return $zones_by_pdns_id; + } + /** * Fetch the zone matching the specific name. * @param string $name of zone to fetch diff --git a/templates/zone.php b/templates/zone.php index 09c395e..381c38f 100644 --- a/templates/zone.php +++ b/templates/zone.php @@ -26,6 +26,7 @@ $cryptokeys = $this->get('cryptokeys'); $allusers = $this->get('allusers'); $replication_types = $this->get('replication_types'); +$catalog_zones = $this->get('catalog_zones'); $local_zone = $this->get('local_zone'); $local_ipv4_ranges = $this->get('local_ipv4_ranges'); $local_ipv6_ranges = $this->get('local_ipv6_ranges'); @@ -378,6 +379,21 @@ +
+ +
+ admin) { ?> + + +

catalog)?>

+ +
+
diff --git a/templates/zones.php b/templates/zones.php index fd5346f..451a5d2 100644 --- a/templates/zones.php +++ b/templates/zones.php @@ -17,6 +17,7 @@ $active_user = $this->get('active_user'); $zones = $this->get('zones'); $replication_types = $this->get('replication_types'); +$catalog_zones = $this->get('catalog_zones'); $soa_templates = $this->get('soa_templates'); $ns_templates = $this->get('ns_templates'); $dnssec_enabled = $this->get('dnssec_enabled'); @@ -200,6 +201,17 @@
+
+ +
+ +
+
diff --git a/views/zone.php b/views/zone.php index b9b86a5..d8482bf 100644 --- a/views/zone.php +++ b/views/zone.php @@ -93,6 +93,7 @@ $accounts = $zone_dir->list_accounts(); $allusers = $user_dir->list_users(); $replication_types = $replication_type_dir->list_replication_types(); +$catalog_zones = $zone_dir->list_zones_by_kind('Producer'); $force_change_review = isset($config['web']['force_change_review']) ? intval($config['web']['force_change_review']) : 0; $force_change_comment = isset($config['web']['force_change_comment']) ? intval($config['web']['force_change_comment']) : 0; $account_whitelist = !empty($config['dns']['classification_whitelist']) ? explode(',', $config['dns']['classification_whitelist']) : []; @@ -215,6 +216,7 @@ redirect(); } elseif(isset($_POST['update_zone']) && ($active_user->admin || $active_user->access_to($zone) == 'administrator')) { $zone->kind = $_POST['kind']; + $zone->catalog = $_POST['catalog']; $zone->account = $_POST['classification']; $zone->update(); $primary_ns = $_POST['primary_ns']; @@ -336,6 +338,7 @@ $content->set('cryptokeys', $cryptokeys); $content->set('allusers', $allusers); $content->set('replication_types', $replication_types); + $content->set('catalog_zones', $catalog_zones); $content->set('local_zone', $local_zone); $content->set('local_ipv4_ranges', $config['dns']['local_ipv4_ranges']); $content->set('local_ipv6_ranges', $config['dns']['local_ipv6_ranges']); diff --git a/views/zones.php b/views/zones.php index 3fc4487..f6ac326 100644 --- a/views/zones.php +++ b/views/zones.php @@ -23,6 +23,7 @@ }); $replication_types = $replication_type_dir->list_replication_types(); +$catalog_zones = $zone_dir->list_zones_by_kind('Producer'); $soa_templates = $template_dir->list_soa_templates(); $ns_templates = $template_dir->list_ns_templates(); $account_whitelist = !empty($config['dns']['classification_whitelist']) ? explode(',', $config['dns']['classification_whitelist']) : []; @@ -40,6 +41,7 @@ $zone->account = trim($_POST['classification']); $zone->dnssec = isset($_POST['dnssec']) ? 1 : 0; $zone->kind = $_POST['kind']; + $zone->catalog = $_POST['catalog']; $zone->nameservers = array(); foreach(preg_split('/[,\s]+/', $_POST['nameservers']) as $nameserver) { $zone->nameservers[] = $nameserver; @@ -68,6 +70,7 @@ $content = new PageSection('zones'); $content->set('zones', $zones); $content->set('replication_types', $replication_types); + $content->set('catalog_zones', $catalog_zones); $content->set('soa_templates', $soa_templates); $content->set('ns_templates', $ns_templates); $content->set('dnssec_enabled', isset($config['dns']['dnssec']) ? $config['dns']['dnssec'] : '0'); From 7bf8441febdc5e0d4315dd4973cdf783694e4131 Mon Sep 17 00:00:00 2001 From: Guillaume-Jean Herbiet Date: Mon, 7 Oct 2024 15:16:09 +0200 Subject: [PATCH 02/12] Update catalog in zone configuration. --- model/zone.php | 3 ++- templates/zone.php | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/model/zone.php b/model/zone.php index 7874ec5..4da03f7 100644 --- a/model/zone.php +++ b/model/zone.php @@ -97,7 +97,8 @@ public function __set($field, $value) { $this->api_rectify = $value; break; case 'catalog': - $this->catalog = $value; + if($value != '') $this->catalog = $value; + break; default: parent::__set($field, $value); } diff --git a/templates/zone.php b/templates/zone.php index 381c38f..956a88a 100644 --- a/templates/zone.php +++ b/templates/zone.php @@ -385,8 +385,8 @@ admin) { ?> From 1c2b409c5263ee13d88cd1817096cd61e0ff6dd9 Mon Sep 17 00:00:00 2001 From: Guillaume-Jean Herbiet Date: Mon, 21 Oct 2024 13:49:07 +0200 Subject: [PATCH 03/12] Fix catalog not being stored on first operation. --- model/zone.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/model/zone.php b/model/zone.php index 4da03f7..17718b7 100644 --- a/model/zone.php +++ b/model/zone.php @@ -48,10 +48,6 @@ class Zone extends Record { */ private $api_rectify = null; /** - * Consumer catalog zone for this zone - */ - private $catalog = null; - /** * List of changes to be applied to the zone when doing ->commit_changes() */ private $changes = array(); @@ -96,9 +92,6 @@ public function __set($field, $value) { case 'api_rectify': $this->api_rectify = $value; break; - case 'catalog': - if($value != '') $this->catalog = $value; - break; default: parent::__set($field, $value); } From 22a81109e6f699cbd18f3073cc0c1beac06a4329 Mon Sep 17 00:00:00 2001 From: Guillaume-Jean Herbiet Date: Mon, 21 Oct 2024 15:48:52 +0200 Subject: [PATCH 04/12] Create new catalog zones. --- config/config-sample.ini | 5 ++ public_html/extra.js | 11 ++++ templates/zones.php | 109 ++++++++++++++++++++------------------- views/zones.php | 25 ++++++--- 4 files changed, 89 insertions(+), 61 deletions(-) diff --git a/config/config-sample.ini b/config/config-sample.ini index c33af91..c159e3d 100644 --- a/config/config-sample.ini +++ b/config/config-sample.ini @@ -127,3 +127,8 @@ local_ipv6_ranges = "fd00::/8 ::1/128" ; path must be a git repository writable by the webserver user. enabled = 0 path = /tmp/dns-export + +[catalog] +; SOA RR content and TTL for the catalog zones +soa = "invalid. invalid. 1 3600 600 2147483646 0" +soa_ttl = 0 diff --git a/public_html/extra.js b/public_html/extra.js index 6659f6b..1e7cbb4 100644 --- a/public_html/extra.js +++ b/public_html/extra.js @@ -903,6 +903,17 @@ $(function() { $('a[href=#create]').tab('show'); } }); + + // Disable superfluous fields for Producer (catalog) zones + $('form.zoneadd, form.zoneeditsoa').each(function() { + var form = $(this); + $('select#kind', form).on('change', function() { + var hide_fields = (this.value == 'Producer') + $('div#catalog-form-group, div#dnssec-form-group, fieldset#soa-fieldset, fieldset#nameservers-fieldset', form).each(function() { + $(this).prop('hidden', hide_fields); + }); + }); + }); $('#changelog-expand-all').on('click', function() { $('table.changelog tbody tr[data-changeset]').each(function() { diff --git a/templates/zones.php b/templates/zones.php index 451a5d2..97354c2 100644 --- a/templates/zones.php +++ b/templates/zones.php @@ -183,65 +183,68 @@

Create zone

- get('active_user')->get_csrf_field(), ESC_NONE) ?> -
- -
- +
+ Zone settings + get('active_user')->get_csrf_field(), ESC_NONE) ?> +
+ +
+ +
-
-
- -
- +
+ +
+ +
-
-
- -
- +
+ +
+ +
-
-
- -
- - - - - - - +
+ +
+ + + + + + + + + - - +
-
- -
- -
-
- + +
+ +
+
+ +
-
- -
+ +
+
SOA
@@ -295,7 +298,7 @@
-
+
Nameservers
diff --git a/views/zones.php b/views/zones.php index f6ac326..84c2b67 100644 --- a/views/zones.php +++ b/views/zones.php @@ -39,20 +39,29 @@ $zone = new Zone; $zone->name = $zonename; $zone->account = trim($_POST['classification']); - $zone->dnssec = isset($_POST['dnssec']) ? 1 : 0; $zone->kind = $_POST['kind']; - $zone->catalog = $_POST['catalog']; - $zone->nameservers = array(); - foreach(preg_split('/[,\s]+/', $_POST['nameservers']) as $nameserver) { - $zone->nameservers[] = $nameserver; - } $soa = new ResourceRecord; - $soa->content = "$_POST[primary_ns] $_POST[contact] ".date('Ymd00')." ".DNSTime::expand($_POST['refresh'])." ".DNSTime::expand($_POST['retry'])." ".DNSTime::expand($_POST['expire'])." ".DNSTime::expand($_POST['default_ttl']); + if ($zone->kind == 'Producer') { + $zone->dnssec = 0; + $zone->catalog = ''; + $zone->nameservers = ['invalid.']; + $soa->content = $config['catalog']['soa']; + $soa_ttl = $config['catalog']['soa_ttl']; + } else { + $zone->dnssec = isset($_POST['dnssec']) ? 1 : 0; + $zone->catalog = $_POST['catalog']; + $zone->nameservers = array(); + foreach(preg_split('/[,\s]+/', $_POST['nameservers']) as $nameserver) { + $zone->nameservers[] = $nameserver; + } + $soa->content = "$_POST[primary_ns] $_POST[contact] ".date('Ymd00')." ".DNSTime::expand($_POST['refresh'])." ".DNSTime::expand($_POST['retry'])." ".DNSTime::expand($_POST['expire'])." ".DNSTime::expand($_POST['default_ttl']); + $soa_ttl = DNSTime::expand($_POST['soa_ttl']); + } $soa->disabled = false; $soaset = new ResourceRecordSet; $soaset->name = $zonename; $soaset->type = 'SOA'; - $soaset->ttl = DNSTime::expand($_POST['soa_ttl']); + $soaset->ttl = $soa_ttl; $soaset->add_resource_record($soa); $zone->add_resource_record_set($soaset); try { From f0689e62f2227b402000439a0e902b30ebaaec86 Mon Sep 17 00:00:00 2001 From: Guillaume-Jean Herbiet Date: Tue, 22 Oct 2024 07:28:28 +0200 Subject: [PATCH 05/12] Change zone configuration from/to catalog from/to any other type: When changing to "Producer" replication type, RFC-recommended `invalid.` namerserver and `soa` from the `catalog` config section are applied. When changing from "Producer" to "Master" or "Native" replication type, the default nameservers template and the the specified SOA are applied. Zone content (all records except SOA and apex NS RRSet) is kept as this does not cause issue to PowerDNS: when a zone is set to "Producer", all non-catalog records are not served. --- templates/zone.php | 290 +++++++++++++++++++++++---------------------- views/zone.php | 86 ++++++++++---- 2 files changed, 214 insertions(+), 162 deletions(-) diff --git a/templates/zone.php b/templates/zone.php index 956a88a..d8f0ea8 100644 --- a/templates/zone.php +++ b/templates/zone.php @@ -364,164 +364,172 @@

Zone configuration

get('active_user')->get_csrf_field(), ESC_NONE) ?> -

Zone settings

-
- -
- admin) { ?> - + + + + + +

kind)?>

- - -

kind)?>

- +
-
-
- -
- admin) { ?> - + + + name != $zone->name) { ?> + + + + + +

catalog)?>

- - -

catalog)?>

- +
-
-
- -
- admin) { ?> - - - - - - - +
+ +
+ admin) { ?> + + + + + + + + + + + +

account)?>

- - - -

account)?>

- +
-
-

Start of authority (SOA)

- admin) { ?> -
- -
- - - - Edit templates + +
kind == "Producer") out(' hidden', ESC_NONE)?>> + Start of authority (SOA) + admin) { ?> +
+ +
+ + + + Edit templates +
-
- -
- -
- admin) { ?> - - -

soa->primary_ns)?>

- + +
+ +
+ admin) { ?> + + +

soa->primary_ns)?>

+ +
-
-
- -
- admin) { ?> - - -

soa->contact)?>

- +
+ +
+ admin) { ?> + + +

soa->contact)?>

+ +
-
-
- -
-

soa->serial)?>

+
+ +
+

soa->serial)?>

+
-
-
- -
- admin) { ?> - - -

soa->refresh))?>

- +
+ +
+ admin) { ?> + + +

soa->refresh))?>

+ +
-
-
- -
- admin) { ?> - - -

soa->retry))?>

- +
+ +
+ admin) { ?> + + +

soa->retry))?>

+ +
-
-
- -
- admin) { ?> - - -

soa->expiry))?>

- +
+ +
+ admin) { ?> + + +

soa->expiry))?>

+ +
-
-
- -
- admin) { ?> - - -

soa->default_ttl))?>

- +
+ +
+ admin) { ?> + + +

soa->default_ttl))?>

+ +
-
-
- -
- admin) { ?> - - -

soa->ttl))?>

- +
+ +
+ admin) { ?> + + +

soa->ttl))?>

+ +
-
-
- admin) { ?> -
- -
- > + +
+ Comment + admin) { ?> +
+ +
+ > +
-
-
-
- +
+
+ +
-
- + +
diff --git a/views/zone.php b/views/zone.php index d8482bf..43799fc 100644 --- a/views/zone.php +++ b/views/zone.php @@ -100,6 +100,7 @@ $force_account_whitelist = !empty($config['dns']['classification_whitelist']) ? 1 : 0; $dnssec_enabled = isset($config['dns']['dnssec']) ? intval($config['dns']['dnssec']) : 0; $dnssec_edit = isset($config['dns']['dnssec_edit']) ? intval($config['dns']['dnssec_edit']) : 1; +$ns_templates = $template_dir->list_ns_templates(); if($_SERVER['REQUEST_METHOD'] == 'POST') { if(isset($_POST['update_rrs'])) { @@ -215,17 +216,27 @@ $active_user->add_alert($alert); redirect(); } elseif(isset($_POST['update_zone']) && ($active_user->admin || $active_user->access_to($zone) == 'administrator')) { + // Update zone settings + $previous_kind = $zone->kind; $zone->kind = $_POST['kind']; - $zone->catalog = $_POST['catalog']; + $zone->catalog = ($zone->kind == 'Producer') ? '' : $_POST['catalog']; $zone->account = $_POST['classification']; $zone->update(); - $primary_ns = $_POST['primary_ns']; - $contact = $_POST['contact']; - $refresh = DNSTime::expand($_POST['refresh']); - $retry = DNSTime::expand($_POST['retry']); - $expiry = DNSTime::expand($_POST['expire']); - $default_ttl = DNSTime::expand($_POST['default_ttl']); - $soa_ttl = DNSTime::expand($_POST['soa_ttl']); + // Update zone SOA and NS records + $json = new StdClass; + $json->actions = array(); + if($zone->kind == 'Producer') { + list($primary_ns, $contact, $serial, $refresh, $retry, $expiry, $default_ttl) = explode(' ', $config['catalog']['soa']); + $soa_ttl = $config['catalog']['soa_ttl']; + } else { + $primary_ns = $_POST['primary_ns']; + $contact = $_POST['contact']; + $refresh = DNSTime::expand($_POST['refresh']); + $retry = DNSTime::expand($_POST['retry']); + $expiry = DNSTime::expand($_POST['expire']); + $default_ttl = DNSTime::expand($_POST['default_ttl']); + $soa_ttl = DNSTime::expand($_POST['soa_ttl']); + } if($zone->soa->primary_ns != $primary_ns || $zone->soa->contact != $contact || $zone->soa->refresh != $refresh @@ -233,19 +244,51 @@ || $zone->soa->expiry != $expiry || $zone->soa->default_ttl != $default_ttl || $zone->soa->ttl != $soa_ttl) { - $record = new StdClass; - $record->content = "$primary_ns $contact {$zone->soa->serial} $refresh $retry $expiry $default_ttl"; - $record->enabled = 'Yes'; - $update = new StdClass; - $update->action = 'update'; - $update->oldname = '@'; - $update->oldtype = 'SOA'; - $update->name = '@'; - $update->type = 'SOA'; - $update->ttl = $soa_ttl; - $update->records = array($record); - $json = new StdClass; - $json->actions = array($update); + $soa_record = new StdClass; + $soa_record->content = "$primary_ns $contact {$zone->soa->serial} $refresh $retry $expiry $default_ttl"; + $soa_record->enabled = 'Yes'; + $soa_update = new StdClass; + $soa_update->action = 'update'; + $soa_update->oldname = '@'; + $soa_update->oldtype = 'SOA'; + $soa_update->name = '@'; + $soa_update->type = 'SOA'; + $soa_update->ttl = $soa_ttl; + $soa_update->records = array($soa_record); + array_push($json->actions, $soa_update); + } + if($previous_kind != $zone->kind) { + $ns_update = new StdClass; + $ns_update->action = 'update'; + $ns_update->oldname = '@'; + $ns_update->oldtype = 'NS'; + $ns_update->name = '@'; + $ns_update->type = 'NS'; + $ns_update->ttl = $default_ttl; + // Use RFC-recommended 'invalid.' as NS for Producer zones + if($zone->kind == 'Producer') { + $ns_record = new StdClass; + $ns_record->content = "invalid."; + $ns_record->enabled = 'Yes'; + $ns_update->records = array($ns_record); + // For Master and Native zones, use the default Nameservers template + // TODO: Allow to chose Nameservers template to apply, like in zone creation + } elseif ($zone->kind == 'Master' || $zone->kind == 'Native') { + foreach($ns_templates as $template) { + if($template->default === 1) { + $ns_update->records = array(); + foreach(preg_split('/[,\s]+/', $template->nameservers) as $nameserver) { + $ns_record = new StdClass; + $ns_record->content = $nameserver; + $ns_record->enabled = 'Yes'; + array_push($ns_update->records, $ns_record); + } + } + } + } + array_push($json->actions, $ns_update); + } + if(count($json->actions) > 0) { $json->comment = $_POST['soa_change_comment']; $zone->process_bulk_json_rrset_update(json_encode($json)); } @@ -343,6 +386,7 @@ $content->set('local_ipv4_ranges', $config['dns']['local_ipv4_ranges']); $content->set('local_ipv6_ranges', $config['dns']['local_ipv6_ranges']); $content->set('soa_templates', $template_dir->list_soa_templates()); + $content->set('ns_templates', $ns_templates); $content->set('dnssec_enabled', $dnssec_enabled); $content->set('dnssec_edit', $dnssec_edit); $content->set('deletion', $deletion); From 7f363cd1a7a5c1caba745e07fd87d857dd478c71 Mon Sep 17 00:00:00 2001 From: Guillaume-Jean Herbiet Date: Tue, 22 Oct 2024 09:59:00 +0200 Subject: [PATCH 06/12] Remove catalog attribute for member zones of former catalog zone. --- model/zonedirectory.php | 16 ++++++++++++++++ views/zone.php | 9 +++++++++ 2 files changed, 25 insertions(+) diff --git a/model/zonedirectory.php b/model/zonedirectory.php index dcdf690..78eda5e 100644 --- a/model/zonedirectory.php +++ b/model/zonedirectory.php @@ -210,6 +210,22 @@ public function list_zones_by_kind($type) { return $zones_by_pdns_id; } + /** + * Fetch the list of member zones having the specified catalog zone as producer + * @param string $catalog zone to list members + * @return array of member Zone objects indexed by pdns_id + */ + public function list_zones_by_catalog($catalog) { + $stmt = $this->database->prepare('SELECT * FROM zone WHERE catalog = ?'); + $stmt->bindParam(1, $catalog, PDO::PARAM_STR); + $stmt->execute(); + $zones_by_pdns_id = array(); + while($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + $zones_by_pdns_id[$row['pdns_id']] = new Zone($row['id'], $row); + } + return $zones_by_pdns_id; + } + /** * Fetch the zone matching the specific name. * @param string $name of zone to fetch diff --git a/views/zone.php b/views/zone.php index 43799fc..b0eedce 100644 --- a/views/zone.php +++ b/views/zone.php @@ -94,6 +94,7 @@ $allusers = $user_dir->list_users(); $replication_types = $replication_type_dir->list_replication_types(); $catalog_zones = $zone_dir->list_zones_by_kind('Producer'); +$member_zones = $zone_dir->list_zones_by_catalog($zone->name); $force_change_review = isset($config['web']['force_change_review']) ? intval($config['web']['force_change_review']) : 0; $force_change_comment = isset($config['web']['force_change_comment']) ? intval($config['web']['force_change_comment']) : 0; $account_whitelist = !empty($config['dns']['classification_whitelist']) ? explode(',', $config['dns']['classification_whitelist']) : []; @@ -292,6 +293,14 @@ $json->comment = $_POST['soa_change_comment']; $zone->process_bulk_json_rrset_update(json_encode($json)); } + // When kind is changed away from Producer, update all member zones + // to no longer be part of the catalog + if($previous_kind === 'Producer' && $previous_kind != $zone->kind) { + foreach($member_zones as $member) { + $member->catalog = ''; + $member->update(); + } + } redirect(); } elseif(isset($_POST['enable_dnssec']) && $active_user->admin && $dnssec_enabled && $dnssec_edit) { $zone->dnssec = 1; From 583021ddef34675a216c243a4535532b31836d2c Mon Sep 17 00:00:00 2001 From: Guillaume-Jean Herbiet Date: Mon, 2 Dec 2024 16:25:08 +0100 Subject: [PATCH 07/12] Fix catalog zone creation when no SOA/NS template is created: * create a 'Producer' SOA template according to RFC specifications * create a 'Producer' NS template according to RFC specifications * prevent 'Producer' templates name change and deletion * allocate 'Producer' templates when 'Producer' replication type is selected, and revert to default (or empty) templatyes when other replication type is selected. * disable and set empty value for 'Catalog zone' and 'DNSSEC' when 'Producer' replication type is selected. --- config/config-sample.ini | 5 ----- migrations/007.php | 2 ++ public_html/extra.js | 42 +++++++++++++++++++++++++++++----------- templates/template.php | 2 +- templates/templates.php | 2 ++ views/zones.php | 25 ++++++++---------------- 6 files changed, 44 insertions(+), 34 deletions(-) diff --git a/config/config-sample.ini b/config/config-sample.ini index c159e3d..c33af91 100644 --- a/config/config-sample.ini +++ b/config/config-sample.ini @@ -127,8 +127,3 @@ local_ipv6_ranges = "fd00::/8 ::1/128" ; path must be a git repository writable by the webserver user. enabled = 0 path = /tmp/dns-export - -[catalog] -; SOA RR content and TTL for the catalog zones -soa = "invalid. invalid. 1 3600 600 2147483646 0" -soa_ttl = 0 diff --git a/migrations/007.php b/migrations/007.php index f047c6b..6e1e476 100644 --- a/migrations/007.php +++ b/migrations/007.php @@ -5,3 +5,5 @@ $this->database->exec("INSERT INTO replication_type VALUES (3, 'Producer', 'A producer or catalog zone is a special zone that store the current list of zones assoicated with it. It can be used by secondary servers to update the list of zones for which they are authoritative')"); +$this->database->exec("INSERT INTO soa_template VALUES (1, 'Producer', 'invalid.', 'invalid.', 3600, 600, 2147483646, 0, 0)"); +$this->database->exec("INSERT INTO ns_template VALUES (1, 'Producer', 'invalid.')"); diff --git a/public_html/extra.js b/public_html/extra.js index 1e7cbb4..3653d3c 100644 --- a/public_html/extra.js +++ b/public_html/extra.js @@ -880,6 +880,37 @@ $(function() { $(this).removeClass('btn-default').addClass('btn-success'); }); + // Handle Producer zones settings restictions + $('button.soa-template:contains(Producer), ' + + 'button.ns-template:contains(Producer)', form).each(function() { + $(this).prop('disabled', true) + }); + $('select#kind', form).on('change', function() { + var zone_kind = this.value + if (zone_kind == 'Producer') { + $('button.soa-template, button.ns-template', form).each(function() { $(this).prop('disabled', true) }); + $('button.soa-template:contains(Producer), ' + + 'button.ns-template:contains(Producer)', form).each(function() { + $(this).prop('disabled', false).click(); + }); + $('#catalog').val(''); + $('#dnssec').prop('checked', false); + + } else { + $('button.soa-template, button.ns-template', form).each(function() { $(this).prop('disabled', false) }); + $('button.soa-template:contains(Producer), ' + + 'button.ns-template:contains(Producer)', form).each(function() { + $.each(this.dataset, function(index, value) { $('#' + index).val('') }); + $(this).removeClass('btn-success').addClass('btn-default').prop('disabled', true); + }); + $('button.soa-template[data-default="1"], ' + + 'button.ns-template[data-default="1"]', form).each(function() { + $(this).click(); + }); + } + $('#catalog, #dnssec').each(function() { $(this).prop('disabled', (zone_kind === 'Producer')) }); + }); + $('input#ipv4_zone_prefix').on('keyup', function(event) { if(event.which == 13) prefill_reverse_ipv4_zone($(this)) }); $('button#ipv4_zone_create').on('click', function() { prefill_reverse_ipv4_zone($('input#ipv4_zone_prefix')) }); $('input#ipv6_zone_prefix').on('keyup', function(event) { if(event.which == 13) prefill_reverse_ipv6_zone($(this)) }); @@ -904,17 +935,6 @@ $(function() { } }); - // Disable superfluous fields for Producer (catalog) zones - $('form.zoneadd, form.zoneeditsoa').each(function() { - var form = $(this); - $('select#kind', form).on('change', function() { - var hide_fields = (this.value == 'Producer') - $('div#catalog-form-group, div#dnssec-form-group, fieldset#soa-fieldset, fieldset#nameservers-fieldset', form).each(function() { - $(this).prop('hidden', hide_fields); - }); - }); - }); - $('#changelog-expand-all').on('click', function() { $('table.changelog tbody tr[data-changeset]').each(function() { show_changes($(this), true); diff --git a/templates/template.php b/templates/template.php index 7823fcf..a0a4db4 100644 --- a/templates/template.php +++ b/templates/template.php @@ -23,7 +23,7 @@
- + name == 'Producer') { ?> readonly>
diff --git a/templates/templates.php b/templates/templates.php index 4615ba9..0cc6b85 100644 --- a/templates/templates.php +++ b/templates/templates.php @@ -55,12 +55,14 @@ name)?> Edit + name != 'Producer') { ?> default) { ?> + name = $zonename; $zone->account = trim($_POST['classification']); + $zone->dnssec = isset($_POST['dnssec']) ? 1 : 0; + $zone->catalog = isset($_POST['catalog']) ? $_POST['catalog'] : null; $zone->kind = $_POST['kind']; - $soa = new ResourceRecord; - if ($zone->kind == 'Producer') { - $zone->dnssec = 0; - $zone->catalog = ''; - $zone->nameservers = ['invalid.']; - $soa->content = $config['catalog']['soa']; - $soa_ttl = $config['catalog']['soa_ttl']; - } else { - $zone->dnssec = isset($_POST['dnssec']) ? 1 : 0; - $zone->catalog = $_POST['catalog']; - $zone->nameservers = array(); - foreach(preg_split('/[,\s]+/', $_POST['nameservers']) as $nameserver) { - $zone->nameservers[] = $nameserver; - } - $soa->content = "$_POST[primary_ns] $_POST[contact] ".date('Ymd00')." ".DNSTime::expand($_POST['refresh'])." ".DNSTime::expand($_POST['retry'])." ".DNSTime::expand($_POST['expire'])." ".DNSTime::expand($_POST['default_ttl']); - $soa_ttl = DNSTime::expand($_POST['soa_ttl']); + $zone->nameservers = array(); + foreach(preg_split('/[,\s]+/', $_POST['nameservers']) as $nameserver) { + $zone->nameservers[] = $nameserver; } + $soa = new ResourceRecord; + $soa->content = "$_POST[primary_ns] $_POST[contact] ".date('Ymd00')." ".DNSTime::expand($_POST['refresh'])." ".DNSTime::expand($_POST['retry'])." ".DNSTime::expand($_POST['expire'])." ".DNSTime::expand($_POST['default_ttl']); $soa->disabled = false; $soaset = new ResourceRecordSet; $soaset->name = $zonename; $soaset->type = 'SOA'; - $soaset->ttl = $soa_ttl; + $soaset->ttl = DNSTime::expand($_POST['soa_ttl']); $soaset->add_resource_record($soa); $zone->add_resource_record_set($soaset); try { From 43876ff0fd4a1d7905e8730742113ede7afdeab8 Mon Sep 17 00:00:00 2001 From: Guillaume-Jean Herbiet Date: Tue, 3 Dec 2024 14:34:11 +0100 Subject: [PATCH 08/12] Set null value when zone is not assigned to any catalog. --- views/zones.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/zones.php b/views/zones.php index 6c6f725..624a7eb 100644 --- a/views/zones.php +++ b/views/zones.php @@ -40,7 +40,7 @@ $zone->name = $zonename; $zone->account = trim($_POST['classification']); $zone->dnssec = isset($_POST['dnssec']) ? 1 : 0; - $zone->catalog = isset($_POST['catalog']) ? $_POST['catalog'] : null; + $zone->catalog = empty($_POST['catalog']) ? null : $_POST['catalog']; $zone->kind = $_POST['kind']; $zone->nameservers = array(); foreach(preg_split('/[,\s]+/', $_POST['nameservers']) as $nameserver) { From 3d87324a17d60bae1c0cd862a0b102f2a4e840b0 Mon Sep 17 00:00:00 2001 From: Guillaume-Jean Herbiet Date: Tue, 3 Dec 2024 14:39:22 +0100 Subject: [PATCH 09/12] Fix templates allocation on catalog zone edition. --- public_html/extra.js | 59 +++++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/public_html/extra.js b/public_html/extra.js index 3653d3c..fc2bb5e 100644 --- a/public_html/extra.js +++ b/public_html/extra.js @@ -867,49 +867,56 @@ $(function() { }); }); + // Add template button functionality on zone add and zone soa edit form + // and handle constraints for 'Producer' zones $('form.zoneadd, form.zoneeditsoa').each(function() { - var form = $(this); - $('button.soa-template[data-default="1"], button.ns-template[data-default="1"]', form).each(function() { - $.each(this.dataset, function(index, value) { $('#' + index).val(value) }); - $(this).removeClass('btn-default').addClass('btn-success'); - }); - $('button.soa-template, button.ns-template', form).on('click', function() { - $.each(this.dataset, function(index, value) { $('#' + index).val(value) }); - $('button', this.parentNode).removeClass('btn-success').addClass('btn-default'); - $(this).removeClass('btn-default').addClass('btn-success'); - }); - // Handle Producer zones settings restictions - $('button.soa-template:contains(Producer), ' + - 'button.ns-template:contains(Producer)', form).each(function() { - $(this).prop('disabled', true) - }); - $('select#kind', form).on('change', function() { - var zone_kind = this.value + function set_zone_settings_constraints(form) { + var zone_kind = $('select#kind', form).val() if (zone_kind == 'Producer') { - $('button.soa-template, button.ns-template', form).each(function() { $(this).prop('disabled', true) }); + $('button.soa-template, button.ns-template', form).each(function() { + $(this).prop('disabled', true) + }); $('button.soa-template:contains(Producer), ' + - 'button.ns-template:contains(Producer)', form).each(function() { + 'button.ns-template:contains(Producer)', form).each(function() { $(this).prop('disabled', false).click(); }); - $('#catalog').val(''); - $('#dnssec').prop('checked', false); - + $('#catalog', form).val(''); + $('#dnssec', form).prop('checked', false); } else { - $('button.soa-template, button.ns-template', form).each(function() { $(this).prop('disabled', false) }); + $('button.soa-template, button.ns-template', form).each(function() { + $(this).prop('disabled', false) + }); $('button.soa-template:contains(Producer), ' + - 'button.ns-template:contains(Producer)', form).each(function() { + 'button.ns-template:contains(Producer)', form).each(function() { $.each(this.dataset, function(index, value) { $('#' + index).val('') }); $(this).removeClass('btn-success').addClass('btn-default').prop('disabled', true); }); $('button.soa-template[data-default="1"], ' + - 'button.ns-template[data-default="1"]', form).each(function() { + 'button.ns-template[data-default="1"]', form).each(function() { $(this).click(); }); } - $('#catalog, #dnssec').each(function() { $(this).prop('disabled', (zone_kind === 'Producer')) }); + $('#catalog, #dnssec', form).each(function() { + $(this).prop('disabled', (zone_kind === 'Producer')) + }); + } + + var form = $(this); + $('button.soa-template[data-default="1"], ' + + 'button.ns-template[data-default="1"]', form).each(function() { + $.each(this.dataset, function(index, value) { $('#' + index).val(value) }); + $(this).removeClass('btn-default').addClass('btn-success'); }); + $('button.soa-template, button.ns-template', form).on('click', function() { + $.each(this.dataset, function(index, value) { $('#' + index).val(value) }); + $('button', this.parentNode).removeClass('btn-success').addClass('btn-default'); + $(this).removeClass('btn-default').addClass('btn-success'); + }); + + set_zone_settings_constraints(form); + $('select#kind', form).on('change', function() { set_zone_settings_constraints(form); }); $('input#ipv4_zone_prefix').on('keyup', function(event) { if(event.which == 13) prefill_reverse_ipv4_zone($(this)) }); $('button#ipv4_zone_create').on('click', function() { prefill_reverse_ipv4_zone($('input#ipv4_zone_prefix')) }); From 831f90fc8441b4599331765864ca1767414115ce Mon Sep 17 00:00:00 2001 From: Guillaume-Jean Herbiet Date: Tue, 3 Dec 2024 15:29:43 +0100 Subject: [PATCH 10/12] Reusable zone name comparison function for sorting. --- model/zonedirectory.php | 13 +++++++++++++ views/zones.php | 5 ----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/model/zonedirectory.php b/model/zonedirectory.php index 78eda5e..68e0171 100644 --- a/model/zonedirectory.php +++ b/model/zonedirectory.php @@ -113,6 +113,16 @@ public function create_zone($zone) { syslog_report(LOG_INFO, "zone={$zone->name};object=zone;action=add;status=succeeded"); } + /** + * Zone name comparison function, that will group zones by TLD, then SLD, etc. + * To be used as callback function with e.g. the "uasort" function + */ + private function compare_zones_by_name($a, $b) { + $aname = implode(',', array_reverse(explode('.', punycode_to_utf8($a->name)))); + $bname = implode(',', array_reverse(explode('.', punycode_to_utf8($b->name)))); + return strnatcasecmp($aname, $bname); + } + /** * List all zones in PowerDNS and update list in database to match. * @param array $include list of extra data to include in response @@ -191,6 +201,7 @@ public function list_zones($include = array()) { } } $this->database->query('COMMIT WORK'); + uasort($zones_by_pdns_id, array($this, 'compare_zones_by_name')); return $zones_by_pdns_id; } @@ -207,6 +218,7 @@ public function list_zones_by_kind($type) { while($row = $stmt->fetch(PDO::FETCH_ASSOC)) { $zones_by_pdns_id[$row['pdns_id']] = new Zone($row['id'], $row); } + uasort($zones_by_pdns_id, array($this, 'compare_zones_by_name')); return $zones_by_pdns_id; } @@ -223,6 +235,7 @@ public function list_zones_by_catalog($catalog) { while($row = $stmt->fetch(PDO::FETCH_ASSOC)) { $zones_by_pdns_id[$row['pdns_id']] = new Zone($row['id'], $row); } + uasort($zones_by_pdns_id, array($this, 'compare_zones_by_name')); return $zones_by_pdns_id; } diff --git a/views/zones.php b/views/zones.php index 624a7eb..31e2256 100644 --- a/views/zones.php +++ b/views/zones.php @@ -16,11 +16,6 @@ ## $zones = $active_user->list_accessible_zones(array('pending_updates')); -usort($zones, function($a, $b) { - $aname = implode(',', array_reverse(explode('.', punycode_to_utf8($a->name)))); - $bname = implode(',', array_reverse(explode('.', punycode_to_utf8($b->name)))); - return strnatcasecmp($aname, $bname); -}); $replication_types = $replication_type_dir->list_replication_types(); $catalog_zones = $zone_dir->list_zones_by_kind('Producer'); From bc56c491602101c86f745cb84d138ed2a7b2d0fd Mon Sep 17 00:00:00 2001 From: Guillaume-Jean Herbiet Date: Tue, 3 Dec 2024 15:35:07 +0100 Subject: [PATCH 11/12] List member zones instead of records for catalog zones. --- templates/zone.php | 32 +++++++++++++++++++++++++++++--- views/zone.php | 1 + 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/templates/zone.php b/templates/zone.php index d8f0ea8..27f7552 100644 --- a/templates/zone.php +++ b/templates/zone.php @@ -27,6 +27,7 @@ $allusers = $this->get('allusers'); $replication_types = $this->get('replication_types'); $catalog_zones = $this->get('catalog_zones'); +$member_zones = $this->get('member_zones'); $local_zone = $this->get('local_zone'); $local_ipv4_ranges = $this->get('local_ipv4_ranges'); $local_ipv6_ranges = $this->get('local_ipv6_ranges'); @@ -51,7 +52,11 @@
-
+
+

Member zones

+ get('active_user')->get_csrf_field(), ESC_NONE) ?> + + + + + + + + + + + + + + + +
Member zoneAction
name))?> +
+
+

Resource records

get('active_user')->get_csrf_field(), ESC_NONE) ?> @@ -380,7 +406,7 @@
-
kind == "Producer") out(' hidden', ESC_NONE)?>> +
admin) { ?> @@ -424,7 +450,7 @@
-
kind == "Producer") out(' hidden', ESC_NONE)?>> +
Start of authority (SOA) admin) { ?>
diff --git a/views/zone.php b/views/zone.php index b0eedce..f09d27f 100644 --- a/views/zone.php +++ b/views/zone.php @@ -391,6 +391,7 @@ $content->set('allusers', $allusers); $content->set('replication_types', $replication_types); $content->set('catalog_zones', $catalog_zones); + $content->set('member_zones', $member_zones); $content->set('local_zone', $local_zone); $content->set('local_ipv4_ranges', $config['dns']['local_ipv4_ranges']); $content->set('local_ipv6_ranges', $config['dns']['local_ipv6_ranges']); From d354dbc0be7e1ef752992c213e6433649ef59122 Mon Sep 17 00:00:00 2001 From: Guillaume-Jean Herbiet Date: Tue, 3 Dec 2024 15:40:15 +0100 Subject: [PATCH 12/12] Add catalog zone column in zones listing array. --- templates/zones.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/templates/zones.php b/templates/zones.php index 97354c2..1e23855 100644 --- a/templates/zones.php +++ b/templates/zones.php @@ -60,6 +60,7 @@ Zone name Serial Replication type + Catalog zone Classification DNSSEC @@ -75,6 +76,7 @@ serial)?> kind)?> + catalog)?> account)?> dnssec ? 'Enabled' : 'Disabled')?>