diff --git a/migrations/007.php b/migrations/007.php new file mode 100644 index 0000000..6e1e476 --- /dev/null +++ b/migrations/007.php @@ -0,0 +1,9 @@ +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')"); + +$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/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..17718b7 100644 --- a/model/zone.php +++ b/model/zone.php @@ -201,6 +201,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 +649,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..68e0171 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) { @@ -111,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 @@ -149,6 +161,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 +169,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}; @@ -188,6 +201,41 @@ 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; + } + + /** + * 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); + } + uasort($zones_by_pdns_id, array($this, 'compare_zones_by_name')); + 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); + } + uasort($zones_by_pdns_id, array($this, 'compare_zones_by_name')); return $zones_by_pdns_id; } diff --git a/public_html/extra.js b/public_html/extra.js index 6659f6b..fc2bb5e 100644 --- a/public_html/extra.js +++ b/public_html/extra.js @@ -867,10 +867,45 @@ $(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() { + + 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:contains(Producer), ' + + 'button.ns-template:contains(Producer)', form).each(function() { + $(this).prop('disabled', false).click(); + }); + $('#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: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', 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() { + $('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'); }); @@ -880,6 +915,9 @@ $(function() { $(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')) }); $('input#ipv6_zone_prefix').on('keyup', function(event) { if(event.which == 13) prefill_reverse_ipv6_zone($(this)) }); @@ -903,7 +941,7 @@ $(function() { $('a[href=#create]').tab('show'); } }); - + $('#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) { ?> + get('cryptokeys'); $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'); @@ -50,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) ?> @@ -363,149 +390,172 @@

Zone configuration

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

Zone settings

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

kind)?>

- - -

kind)?>

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

catalog)?>

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

account)?>

- - - -

account)?>

- +
-
-

Start of authority (SOA)

- admin) { ?> -
- -
- - - - Edit templates + +
+ 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/templates/zones.php b/templates/zones.php index fd5346f..1e23855 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'); @@ -59,6 +60,7 @@ Zone name Serial Replication type + Catalog zone Classification DNSSEC @@ -74,6 +76,7 @@ serial)?> kind)?> + catalog)?> account)?> dnssec ? 'Enabled' : 'Disabled')?> @@ -182,54 +185,68 @@

Create zone

- get('active_user')->get_csrf_field(), ESC_NONE) ?> -
- -
- +
+ Zone settings + get('active_user')->get_csrf_field(), ESC_NONE) ?> +
+ +
+ +
-
-
- -
- +
+ +
+ +
-
-
- -
- - - - - - - +
+ +
+ +
+
+
+ +
+ + + + + + + + + - - +
-
- -
- -
-
- + +
+ +
+
+ +
-
- -
+ +
+
SOA
@@ -283,7 +300,7 @@
-
+
Nameservers
diff --git a/views/zone.php b/views/zone.php index b9b86a5..f09d27f 100644 --- a/views/zone.php +++ b/views/zone.php @@ -93,12 +93,15 @@ $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'); +$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']) : []; $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'])) { @@ -214,16 +217,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 = ($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 @@ -231,22 +245,62 @@ || $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)); } + // 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; @@ -336,10 +390,13 @@ $content->set('cryptokeys', $cryptokeys); $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']); $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); diff --git a/views/zones.php b/views/zones.php index 3fc4487..31e2256 100644 --- a/views/zones.php +++ b/views/zones.php @@ -16,13 +16,9 @@ ## $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'); $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']) : []; @@ -39,6 +35,7 @@ $zone->name = $zonename; $zone->account = trim($_POST['classification']); $zone->dnssec = isset($_POST['dnssec']) ? 1 : 0; + $zone->catalog = empty($_POST['catalog']) ? null : $_POST['catalog']; $zone->kind = $_POST['kind']; $zone->nameservers = array(); foreach(preg_split('/[,\s]+/', $_POST['nameservers']) as $nameserver) { @@ -68,6 +65,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');