]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
auth: implement nested catalogs
authorKees Monshouwer <mind04@monshouwer.org>
Wed, 22 Jan 2025 22:57:54 +0000 (23:57 +0100)
committerKees Monshouwer <mind04@monshouwer.org>
Fri, 24 Apr 2026 09:43:05 +0000 (11:43 +0200)
Signed-off-by: Kees Monshouwer <mind04@monshouwer.org>
19 files changed:
docs/catalog.rst
docs/settings.rst
modules/gmysqlbackend/gmysqlbackend.cc
modules/godbcbackend/godbcbackend.cc
modules/gpgsqlbackend/gpgsqlbackend.cc
modules/gsqlite3backend/gsqlite3backend.cc
modules/lmdbbackend/lmdbbackend.cc
pdns/auth-catalogzone.cc
pdns/auth-catalogzone.hh
pdns/auth-main.cc
pdns/auth-secondarycommunicator.cc
pdns/backends/gsql/gsqlbackend.cc
pdns/pdnsutil.cc
regression-tests/backends/gmysql-master
regression-tests/backends/gmysql-slave
regression-tests/backends/lmdb-master
regression-tests/backends/lmdb-slave
regression-tests/tests/zone-variants/expected_result.lmdb
regression-tests/zones/catalog2.invalid [new file with mode: 0644]

index fcdac451839263c313879a16965209d2cc18b892..9f711e0ef38930bab80083d1e31dc65e160b9aa6 100644 (file)
@@ -119,6 +119,8 @@ or, prior to version 5.0:
   pdnsutil set-catalog example.com catalog.example
   pdnsutil set-kind example.com primary
 
+Since version 5.1.0 a catalog zone can be a member of another catalog.
+
 Setting catalog values is supported in the :doc:`API <http-api/zone>`, by setting the ``catalog`` property in the zone properties.
 Setting the catalog to an empty ``""`` removes the member zone from the catalog it is in.
 
index 85e7c9312311dad797f6228e637a17562cd87101..3defb4a2a3a811ef4569330f7883d897f26986bc 100644 (file)
@@ -1366,6 +1366,18 @@ Allow this many DNS queries in a single TCP transaction. 0 means
 unlimited. Note that exchanges related to an AXFR or IXFR are not
 affected by this setting.
 
+.. _setting-member-catalog-group
+
+``member-catalog-group``
+------------------------
+
+-  String
+-  Default: pdns-member-catalog
+
+.. versionadded:: 5.1.0
+
+Catalog group used to signal that a member zone is a catalog.
+
 .. _setting-module-dir:
 
 ``module-dir``
index 17fb9fb45e65798c523572ed92c4688b5a1d4eb8..b540bf72fbd6f725cbe7b73dceded8f8ce6307ca 100644 (file)
@@ -143,8 +143,8 @@ public:
     declare(suffix, "update-serial-query", "", "update domains set notified_serial=? where id=?");
     declare(suffix, "update-lastcheck-query", "", "update domains set last_check=? where id=?");
     declare(suffix, "info-all-primary-query", "", "select d.id, d.name, d.type, d.notified_serial,d.options, d.catalog,r.content from records r join domains d on r.domain_id=d.id and r.name=d.name where r.type='SOA' and r.disabled=0 and d.type in ('MASTER', 'PRODUCER') order by d.id");
-    declare(suffix, "info-producer-members-query", "", "select domains.id, domains.name, domains.options from records join domains on records.domain_id=domains.id and records.name=domains.name where domains.type='MASTER' and domains.catalog=? and records.type='SOA' and records.disabled=0");
-    declare(suffix, "info-consumer-members-query", "", "select id, name, options, master from domains where type='SLAVE' and catalog=?");
+    declare(suffix, "info-producer-members-query", "", "select domains.id, domains.name, domains.type, domains.options from records join domains on records.domain_id=domains.id and records.name=domains.name where domains.type in ('MASTER', 'PRODUCER') and domains.catalog=? and records.type='SOA' and records.disabled=0");
+    declare(suffix, "info-consumer-members-query", "", "select id, name, type, options, master from domains where type in ('SLAVE', 'CONSUMER') and catalog=?");
     declare(suffix, "delete-domain-query", "", "delete from domains where name=?");
     declare(suffix, "delete-zone-query", "", "delete from records where domain_id=?");
     declare(suffix, "delete-rrset-query", "", "delete from records where domain_id=? and name=? and type=?");
index ff2e6c478548aae938c385cebf41b6d8936cbb53..d46fc5d616909a7eb11cc46291aa6458797d5c9f 100644 (file)
@@ -122,8 +122,8 @@ public:
     declare(suffix, "update-serial-query", "", "update domains set notified_serial=? where id=?");
     declare(suffix, "update-lastcheck-query", "", "update domains set last_check=? where id=?");
     declare(suffix, "info-all-primary-query", "", "select domains.id, domains.name, domains.type, domains.notified_serial, domains.options, domains.catalog, records.content from records join domains on records.domain_id=domains.id and records.name=domains.name where records.type='SOA' and records.disabled=0 and domains.type in ('MASTER', 'PRODUCER') order by domains.id");
-    declare(suffix, "info-producer-members-query", "", "select domains.id, domains.name, domains.options from records join domains on records.domain_id=domains.id and records.name=domains.name where domains.type='MASTER' and domains.catalog=? and records.type='SOA' and records.disabled=0");
-    declare(suffix, "info-consumer-members-query", "", "select id, name, options, master from domains where type='SLAVE' and catalog=?");
+    declare(suffix, "info-producer-members-query", "", "select domains.id, domains.name, domains.type, domains.options from records join domains on records.domain_id=domains.id and records.name=domains.name where domains.type in ('MASTER', 'PRODUCER') and domains.catalog=? and records.type='SOA' and records.disabled=0");
+    declare(suffix, "info-consumer-members-query", "", "select id, name, type, options, master from domains where type in ('SLAVE', 'CONSUMER') and catalog=?");
     declare(suffix, "delete-domain-query", "", "delete from domains where name=?");
     declare(suffix, "delete-zone-query", "", "delete from records where domain_id=?");
     declare(suffix, "delete-rrset-query", "", "delete from records where domain_id=? and name=? and type=?");
index ce5d452e846ca96847550c2e5c7fcd497ddf096a..d82c0eb6440f9412d8445f5a33d47865a29c025e 100644 (file)
@@ -150,8 +150,8 @@ public:
     declare(suffix, "update-serial-query", "", "update domains set notified_serial=$1 where id=$2");
     declare(suffix, "update-lastcheck-query", "", "update domains set last_check=$1 where id=$2");
     declare(suffix, "info-all-primary-query", "", "select domains.id, domains.name, domains.type, domains.notified_serial, domains.options, domains.catalog, records.content from records join domains on records.domain_id=domains.id and records.name=domains.name where records.type='SOA' and records.disabled=false and domains.type in ('MASTER', 'PRODUCER') order by domains.id");
-    declare(suffix, "info-producer-members-query", "", "select domains.id, domains.name, domains.options from records join domains on records.domain_id=domains.id and records.name=domains.name where domains.type='MASTER' and domains.catalog=$1 and records.type='SOA' and records.disabled=false");
-    declare(suffix, "info-consumer-members-query", "", "select id, name, options, master from domains where type='SLAVE' and catalog=$1");
+    declare(suffix, "info-producer-members-query", "", "select domains.id, domains.name, domains.type, domains.options from records join domains on records.domain_id=domains.id and records.name=domains.name where domains.type in ('MASTER', 'PRODUCER') and domains.catalog=$1 and records.type='SOA' and records.disabled=false");
+    declare(suffix, "info-consumer-members-query", "", "select id, name, type, options, master from domains where type in ('SLAVE', 'CONSUMER') and catalog=$1");
     declare(suffix, "delete-domain-query", "", "delete from domains where name=$1");
     declare(suffix, "delete-zone-query", "", "delete from records where domain_id=$1");
     declare(suffix, "delete-rrset-query", "", "delete from records where domain_id=$1 and name=$2 and type=$3");
index 8d421b62d1116b508c838c0148e80c21a0076a5e..9bf61fca1a086559a5fa1992e88abe9c6c6dd883 100644 (file)
@@ -135,8 +135,8 @@ public:
     declare(suffix, "update-serial-query", "", "update domains set notified_serial=:serial where id=:domain_id");
     declare(suffix, "update-lastcheck-query", "", "update domains set last_check=:last_check where id=:domain_id");
     declare(suffix, "info-all-primary-query", "", "select domains.id, domains.name, domains.type, domains.notified_serial, domains.options, domains.catalog, records.content from records join domains on records.domain_id=domains.id and records.name=domains.name where records.type='SOA' and records.disabled=0 and domains.type in ('MASTER', 'PRODUCER') order by domains.id");
-    declare(suffix, "info-producer-members-query", "", "select domains.id, domains.name, domains.options from records join domains on records.domain_id=domains.id and records.name=domains.name where domains.type='MASTER' and domains.catalog=:catalog and records.type='SOA' and records.disabled=0");
-    declare(suffix, "info-consumer-members-query", "", "select id, name, options, master from domains where type='SLAVE' and catalog=:catalog");
+    declare(suffix, "info-producer-members-query", "", "select domains.id, domains.name, domains.type, domains.options from records join domains on records.domain_id=domains.id and records.name=domains.name where domains.type in ('MASTER', 'PRODUCER') and domains.catalog=:catalog and records.type='SOA' and records.disabled=0");
+    declare(suffix, "info-consumer-members-query", "", "select id, name, type, options, master from domains where type in ('SLAVE', 'CONSUMER') and catalog=:catalog");
     declare(suffix, "delete-domain-query", "", "delete from domains where name=:domain");
     declare(suffix, "delete-zone-query", "", "delete from records where domain_id=:domain_id");
     declare(suffix, "delete-rrset-query", "", "delete from records where domain_id=:domain_id and name=:qname and type=:qtype");
index a43105427099cea85a4670dbd5109482aac55bb2..7cba4a6490863051a9838f4153166d7d5d058efd 100644 (file)
@@ -2457,16 +2457,26 @@ bool LMDBBackend::getCatalogMembers(const ZoneName& catalog, vector<CatalogInfo>
 
   try {
     getAllDomainsFiltered(&scratch, [this, &catalog, &members, &type](DomainInfo& di) {
-      if ((type == CatalogInfo::CatalogType::Producer && di.kind != DomainInfo::Primary) || (type == CatalogInfo::CatalogType::Consumer && di.kind != DomainInfo::Secondary) || di.catalog != catalog) {
+      if ((type == CatalogInfo::CatalogType::Producer && !di.isPrimaryType()) || (type == CatalogInfo::CatalogType::Consumer && !di.isSecondaryType()) || di.catalog != catalog) {
         return false;
       }
 
+      if (di.isCatalogType() && di.zone == di.catalog) {
+        SLOG(g_log << Logger::Warning << __PRETTY_FUNCTION__ << " catalog '" << di.zone << "' cannot be a member of itself" << endl,
+             d_slog->info(Logr::Warning, "catalog cannot be a member of itself", "catalog", Logging::Loggable(di.zone)));
+        members.clear();
+        throw getCatalogMembersReturnFalseException();
+      }
+
       CatalogInfo ci;
       ci.d_id = di.id;
       ci.d_zone = di.zone;
       ci.d_primaries = di.primaries;
       try {
         ci.fromJson(di.options, type);
+        if (di.isCatalogType()) {
+          ci.addGroup(g_memberCatalogGroup);
+        }
       }
       catch (const std::runtime_error& e) {
         SLOG(g_log << Logger::Warning << __PRETTY_FUNCTION__ << " options '" << di.options << "' for zone '" << di.zone << "' is no valid JSON: " << e.what() << endl,
index 68cd4c9eb43b68f55f648849362d6e768425c296..828d9f6137a9870bdc9de73ee5a638c6276c54fd 100644 (file)
@@ -117,7 +117,7 @@ std::string CatalogInfo::toJson() const
 void CatalogInfo::updateCatalogHash(CatalogHashMap& hashes, const DomainInfo& di)
 {
   CatalogInfo ci;
-  hashes[di.catalog].process(std::to_string(di.id) + di.zone.toLogString());
+  hashes[di.catalog].process(std::to_string(di.id) + di.zone.toLogString() + DomainInfo::getKindString(di.kind));
   if (ci.parseJson(di.options, CatalogType::Producer)) {
     hashes[di.catalog].process(ci.d_doc["producer"].dump());
   }
index c8ee041476a3dc8c13396b5fd094fa9c509b0d13..f16368dfa6115f3f4956de80d16e548b02fe9336 100644 (file)
@@ -58,6 +58,7 @@ public:
   void fromJson(const std::string& json, CatalogType type);
   std::string toJson() const;
   void setType(CatalogType type) { d_type = type; }
+  void addGroup(const std::string& group) { d_group.insert(group); }
 
   static void updateCatalogHash(CatalogHashMap& hashes, const DomainInfo& di);
   DNSName getUnique() const { return DNSName(toBase32Hex(hashQNameWithSalt(std::to_string(d_id), 0, DNSName(d_zone)))); } // salt with domain id to detect recreated zones
@@ -81,3 +82,5 @@ private:
 
   bool parseJson(const std::string& json, CatalogType type);
 };
+
+extern std::string g_memberCatalogGroup;
index 77e9cc8d54c62a59dd9be7f98e73c2a97582f1ab..37a3b74c170e190b9707cc5784d3b411f450b6da 100644 (file)
@@ -119,6 +119,7 @@ bool g_doGssTSIG;
 bool g_views;
 bool g_slogStructured{false};
 static Logger::Urgency s_logUrgency;
+std::string g_memberCatalogGroup;
 typedef Distributor<DNSPacket, DNSPacket, PacketHandler> DNSDistributor;
 
 ArgvMap theArg;
@@ -341,6 +342,7 @@ static void declareArguments()
   ::arg().setSwitch("consistent-backends", "Assume individual zones are not divided over backends. Send only ANY lookup operations to the backend to reduce the number of lookups") = "yes";
 
   ::arg().set("default-catalog-zone", "Catalog zone to assign newly created primary zones (via the API) to") = "";
+  ::arg().set("member-catalog-group", "Catalog group used to signal that a member zone is a catalog") = "pdns-member-catalog";
 
 #ifdef ENABLE_GSS_TSIG
   ::arg().setSwitch("enable-gss-tsig", "Enable GSS TSIG processing") = "no";
@@ -779,6 +781,7 @@ static void mainthread()
   g_doGssTSIG = ::arg().mustDo("enable-gss-tsig");
 #endif
   g_views = ::arg().mustDo("views");
+  g_memberCatalogGroup = ::arg()["member-catalog-group"];
 
   DNSPacket::s_udpTruncationThreshold = std::max(512, ::arg().asNum("udp-truncation-threshold"));
   DNSPacket::s_doEDNSSubnetProcessing = ::arg().mustDo("edns-subnet-processing");
index 34f6504b476f06edbec96cdb9cbc0a9e1960d3db..4d8488f9f70560322f9b9769269fe907a2f5d691 100644 (file)
@@ -137,6 +137,7 @@ static bool catalogDiff(const XFRContext& ctx, vector<CatalogInfo>& fromXFR, vec
         CatalogInfo ciDB = *db;
         if (ciDB.d_unique.empty() || ciXFR.d_unique == ciDB.d_unique) { // update
           bool doOptions{false};
+          bool doType{false};
 
           if (ciDB.d_unique.empty()) { // set unique
             SLOG(g_log << Logger::Warning << ctx.logPrefix << "set unique, zone '" << ciXFR.d_zone << "' is now a member" << endl,
@@ -155,6 +156,7 @@ static bool catalogDiff(const XFRContext& ctx, vector<CatalogInfo>& fromXFR, vec
           if (ciXFR.d_group != ciDB.d_group) { // update group
             SLOG(g_log << Logger::Warning << ctx.logPrefix << "update group for zone '" << ciXFR.d_zone << "' to '" << boost::join(ciXFR.d_group, ", ") << "'" << endl,
                  ctx.slog->info(Logr::Warning, "Catalog-Zone: update group", "zone", Logging::Loggable(ciXFR.d_zone), "group", Logging::Loggable(boost::join(ciXFR.d_group, ", ")))); // can't apply Logging::IterLoggable on set
+            doType = !empty(g_memberCatalogGroup) && ((ciXFR.d_group.count(g_memberCatalogGroup) != 0) != (ciDB.d_group.count(g_memberCatalogGroup) != 0));
             ciDB.d_group = ciXFR.d_group;
             doOptions = true;
           }
@@ -171,6 +173,19 @@ static bool catalogDiff(const XFRContext& ctx, vector<CatalogInfo>& fromXFR, vec
             ctx.domain.backend->setOptions(ciXFR.d_zone, ciDB.toJson());
           }
 
+          if (doType) { // update zone type
+            if (doTransaction && (inTransaction = ctx.domain.backend->startTransaction(ctx.domain.zone))) {
+              SLOG(g_log << Logger::Warning << ctx.logPrefix << "backend transaction started" << endl,
+                   ctx.slog->info(Logr::Warning, "Catalog-Zone: backend transaction started"));
+              doTransaction = false;
+            }
+
+            DomainInfo::DomainKind kind = ciXFR.d_group.count(g_memberCatalogGroup) != 0 ? DomainInfo::Consumer : DomainInfo::Secondary;
+            SLOG(g_log << Logger::Warning << ctx.logPrefix << "update type to '" << DomainInfo::getKindString(kind) << "' for zone '" << ciXFR.d_zone << "'" << endl,
+                 ctx.slog->info(Logr::Warning, "Catalog-Zone: update type", "zone", Logging::Loggable(ciXFR.d_zone), "type", Logging::Loggable(kind)));
+            ctx.domain.backend->setKind(ciXFR.d_zone, kind);
+          }
+
           if (ctx.domain.primaries != ciDB.d_primaries) { // update primaries
             if (doTransaction && (inTransaction = ctx.domain.backend->startTransaction(ctx.domain.zone))) {
               SLOG(g_log << Logger::Warning << ctx.logPrefix << "backend transaction started" << endl,
@@ -277,7 +292,8 @@ static bool catalogDiff(const XFRContext& ctx, vector<CatalogInfo>& fromXFR, vec
 
         SLOG(g_log << Logger::Warning << ctx.logPrefix << "create zone '" << ciCreate.d_zone << "'" << endl,
              ctx.slog->info(Logr::Warning, "Catalog-Zone: create zone", "zone", Logging::Loggable(ciCreate.d_zone)));
-        ctx.domain.backend->createDomain(ciCreate.d_zone, DomainInfo::Secondary, ciCreate.d_primaries, "");
+        d.kind = !empty(g_memberCatalogGroup) && ciCreate.d_group.count(g_memberCatalogGroup) != 0 ? DomainInfo::Consumer : DomainInfo::Secondary;
+        ctx.domain.backend->createDomain(ciCreate.d_zone, d.kind, ciCreate.d_primaries, "");
 
         ctx.domain.backend->setPrimaries(ciCreate.d_zone, ctx.domain.primaries);
         ctx.domain.backend->setOptions(ciCreate.d_zone, ciCreate.toJson());
@@ -471,7 +487,7 @@ static bool catalogProcess(const XFRContext& ctx, vector<DNSResourceRecord>& rrs
     return false;
   }
 
-  // Get catalog ifo from db
+  // Get catalog info from db
   if (!ctx.domain.backend->getCatalogMembers(ctx.domain.zone, fromDB, CatalogInfo::CatalogType::Consumer)) {
     return false;
   }
index 765709dc9c4ee8c35aeea8af70461d5331b0e536..45698013515b2d8b0890e7d0a84d24aa2e199b8d 100644 (file)
@@ -539,7 +539,7 @@ void GSQLBackend::getUnfreshSecondaryInfos(vector<DomainInfo>* unfreshDomains)
 void GSQLBackend::getUpdatedPrimaries(vector<DomainInfo>& updatedDomains, std::unordered_set<DNSName>& catalogs, CatalogHashMap& catalogHashes)
 {
   /*
-    list all domains that need notifications for which we are promary, and insert into
+    list all domains that need notifications for which we are primary, and insert into
     updatedDomains: id, name, notified_serial, serial
   */
 
@@ -581,6 +581,8 @@ void GSQLBackend::getUpdatedPrimaries(vector<DomainInfo>& updatedDomains, std::u
       continue;
     }
 
+    di.kind = DomainInfo::stringToKind(row[2]);
+
     try {
       pdns::checked_stoi_into(di.id, row[0]);
     }
@@ -594,24 +596,27 @@ void GSQLBackend::getUpdatedPrimaries(vector<DomainInfo>& updatedDomains, std::u
       di.catalog = ZoneName(row[5]);
     }
     catch (const std::runtime_error& e) {
-      SLOG(g_log << Logger::Warning << __PRETTY_FUNCTION__ << " zone name '" << row[5] << "' is not a valid DNS name: " << e.what() << endl,
-           d_slog->error(Logr::Warning, e.what(), "zone name is not a valid DNS name", "zone", Logging::Loggable(row[5])));
+      SLOG(g_log << Logger::Warning << __PRETTY_FUNCTION__ << " catalog name '" << row[5] << "' is not a valid DNS name: " << e.what() << endl,
+           d_slog->error(Logr::Warning, e.what(), "catalog name is not a valid DNS name", "zone", Logging::Loggable(row[5])));
       continue;
     }
     catch (PDNSException& ae) {
-      SLOG(g_log << Logger::Warning << __PRETTY_FUNCTION__ << " zone name '" << row[5] << "' is not a valid DNS name: " << ae.reason << endl,
-           d_slog->error(Logr::Warning, ae.reason, "zone name is not a valid DNS name", "zone", Logging::Loggable(row[5])));
+      SLOG(g_log << Logger::Warning << __PRETTY_FUNCTION__ << " catalog name '" << row[5] << "' is not a valid DNS name: " << ae.reason << endl,
+           d_slog->error(Logr::Warning, ae.reason, "catalog name is not a valid DNS name", "zone", Logging::Loggable(row[5])));
       continue;
     }
 
-    if (pdns_iequals(row[2], "PRODUCER")) {
+    if (di.kind == DomainInfo::Producer) {
       catalogs.insert(di.zone.operator const DNSName&());
       catalogHashes[di.zone].process("");
-      continue; // Producer freshness check is performed elsewhere
+      if (di.catalog.empty()) {
+        continue; // Producer is no catalog member, freshness check is performed elsewhere
+      }
     }
-    else if (!pdns_iequals(row[2], "MASTER")) {
+    else if (di.kind != DomainInfo::Primary) {
       SLOG(g_log << Logger::Warning << __PRETTY_FUNCTION__ << " type '" << row[2] << "' for zone '" << di.zone << "' is no primary type" << endl,
            d_slog->info(Logr::Warning, "zone type is not fit for a primary", "zone", Logging::Loggable(di.zone), "type", Logging::Loggable(row[2])));
+      continue;
     }
 
     try {
@@ -626,6 +631,10 @@ void GSQLBackend::getUpdatedPrimaries(vector<DomainInfo>& updatedDomains, std::u
       continue;
     }
 
+    if (di.kind == DomainInfo::Producer) {
+      continue; // Producer is a catalog member, freshness check is performed elsewhere
+    }
+
     try {
       pdns::checked_stoi_into(di.notified_serial, row[3]);
     }
@@ -692,12 +701,12 @@ bool GSQLBackend::getCatalogMembers(const ZoneName& catalog, vector<CatalogInfo>
   }
 
   members.reserve(d_result.size());
-  for (const auto& row : d_result) { // id, zone, options, [master]
+  for (const auto& row : d_result) { // id, zone, type, options, [master]
     if (type == CatalogInfo::CatalogType::Producer) {
-      ASSERT_ROW_COLUMNS("info-producer/consumer-members-query", row, 3);
+      ASSERT_ROW_COLUMNS("info-producer/consumer-members-query", row, 4);
     }
     else {
-      ASSERT_ROW_COLUMNS("info-producer/consumer-members-query", row, 4);
+      ASSERT_ROW_COLUMNS("info-producer/consumer-members-query", row, 5);
     }
 
     CatalogInfo ci;
@@ -718,6 +727,16 @@ bool GSQLBackend::getCatalogMembers(const ZoneName& catalog, vector<CatalogInfo>
       return false;
     }
 
+    auto kind = DomainInfo::stringToKind(row[2]);
+    auto isCatalog = kind == DomainInfo::Producer || kind == DomainInfo::Consumer;
+
+    if (isCatalog && ci.d_zone == catalog) {
+      SLOG(g_log << Logger::Warning << __PRETTY_FUNCTION__ << " catalog '" << ci.d_zone << "' cannot be a member of itself" << endl,
+           d_slog->info(Logr::Warning, "catalog cannot be a member of itself", "catalog", Logging::Loggable(ci.d_zone)));
+      members.clear();
+      return false;
+    }
+
     try {
       pdns::checked_stoi_into(ci.d_id, row[0]);
     }
@@ -729,18 +748,21 @@ bool GSQLBackend::getCatalogMembers(const ZoneName& catalog, vector<CatalogInfo>
     }
 
     try {
-      ci.fromJson(row[2], type);
+      ci.fromJson(row[3], type);
+      if (isCatalog) {
+        ci.addGroup(g_memberCatalogGroup);
+      }
     }
     catch (const std::runtime_error& e) {
-      SLOG(g_log << Logger::Warning << __PRETTY_FUNCTION__ << " options '" << row[2] << "' for zone '" << ci.d_zone << "' is no valid JSON: " << e.what() << endl,
-           d_slog->error(Logr::Warning, e.what(), "catalog 'options' field is not valid JSON", "zone", Logging::Loggable(ci.d_zone), "field", Logging::Loggable(row[2])));
+      SLOG(g_log << Logger::Warning << __PRETTY_FUNCTION__ << " options '" << row[3] << "' for zone '" << ci.d_zone << "' is no valid JSON: " << e.what() << endl,
+           d_slog->error(Logr::Warning, e.what(), "catalog 'options' field is not valid JSON", "zone", Logging::Loggable(ci.d_zone), "field", Logging::Loggable(row[3])));
       members.clear();
       return false;
     }
 
-    if (row.size() >= 4) { // Consumer only
+    if (type == CatalogInfo::CatalogType::Consumer) { // Consumer only
       vector<string> primaries;
-      stringtok(primaries, row[3], ", \t");
+      stringtok(primaries, row[4], ", \t");
       for (const auto& m : primaries) {
         try {
           ci.d_primaries.emplace_back(m, 53);
index 69d021403f97bf67629a247c843de6a61a53fbc1..6be4faddaaf1f68bab57af8e8dc1e8f2beb83ddb 100644 (file)
@@ -56,6 +56,7 @@ AuthPacketCache PC;
 AuthQueryCache QC;
 AuthZoneCache g_zoneCache;
 uint16_t g_maxNSEC3Iterations{0};
+std::string g_memberCatalogGroup;
 
 namespace po = boost::program_options;
 po::variables_map g_vm;
index e90f95e1523638cd35c2c7961f3faa9bbbb9eeef..1721e7faf1ef8a007d82e23f2e170f76e64732a5 100644 (file)
@@ -41,6 +41,11 @@ __EOF__
                        $PDNSUTIL --config-dir=. --config-name=gmysql load-zone catalog.invalid zones/catalog.invalid
                        $PDNSUTIL --config-dir=. --config-name=gmysql set-kind catalog.invalid producer
 
+                       $PDNSUTIL --config-dir=. --config-name=gmysql load-zone catalog2.invalid zones/catalog2.invalid
+                       $PDNSUTIL --config-dir=. --config-name=gmysql set-kind catalog2.invalid producer
+                       $PDNSUTIL --config-dir=. --config-name=gmysql set-catalog catalog2.invalid catalog.invalid
+                       $PDNSUTIL --config-dir=. --config-name=gmysql set-catalog minimal.com catalog2.invalid
+
                        $PDNSUTIL --config-dir=. --config-name=gmysql set-option test.com producer coo other-catalog.invalid
                        $PDNSUTIL --config-dir=. --config-name=gmysql set-option test.com producer unique 123
                        $PDNSUTIL --config-dir=. --config-name=gmysql set-option tsig.com producer group pdns-group-x pdns-group-y
index e86c807c6c30aab1455dbae9eb2870d47d3683ca..9f652f517b8e0b6e89c1f91d22f6eeb56601507b 100644 (file)
@@ -70,7 +70,7 @@ __EOF__
        # setup catalog zone
        if [ $zones -ne 1 ] # detect root tests
        then
-               zones=$((zones+1))
+               zones=$((zones+2))
                $PDNSUTIL --config-dir=. --config-name=gmysql2 create-secondary-zone catalog.invalid 127.0.0.1:$port
                $PDNSUTIL --config-dir=. --config-name=gmysql2 set-kind catalog.invalid consumer
 
index f04c31791b5fee722736cba01af53b09ba3d39d4..98197c50f0a2ee219afdd7382e22f36ba22b9100 100644 (file)
@@ -85,6 +85,11 @@ __EOF__
             $RUNWRAPPER_PDNSUTIL $PDNSUTIL --config-dir=. --config-name=lmdb load-zone catalog.invalid zones/catalog.invalid
             $RUNWRAPPER_PDNSUTIL $PDNSUTIL --config-dir=. --config-name=lmdb set-kind catalog.invalid producer
 
+            $RUNWRAPPER_PDNSUTIL $PDNSUTIL --config-dir=. --config-name=lmdb load-zone catalog2.invalid zones/catalog2.invalid
+            $RUNWRAPPER_PDNSUTIL $PDNSUTIL --config-dir=. --config-name=lmdb set-kind catalog2.invalid producer
+            $RUNWRAPPER_PDNSUTIL $PDNSUTIL --config-dir=. --config-name=lmdb set-catalog catalog2.invalid catalog.invalid
+            $RUNWRAPPER_PDNSUTIL $PDNSUTIL --config-dir=. --config-name=lmdb set-catalog minimal.com$plusvariant catalog2.invalid
+
             $RUNWRAPPER_PDNSUTIL $PDNSUTIL --config-dir=. --config-name=lmdb set-options-json test.com$plusvariant '{"producer":{"coo":"other-catalog.invalid","unique":"123"}}'
             $RUNWRAPPER_PDNSUTIL $PDNSUTIL --config-dir=. --config-name=lmdb set-options-json tsig.com$plusvariant '{"producer":{"group":["pdns-group-x","pdns-group-y"]}}'
         fi
index cbbbcc0c84a054aaf014989cca13de1dfd8d174e..fb1e0d5a70a0e020286b4d03e45a1ac6e010d7bf 100644 (file)
@@ -47,7 +47,7 @@ __EOF__
         # setup catalog zone
         if [ $zones -ne 1 ] # detect root tests
         then
-                zones=$((zones+1))
+                zones=$((zones+2))
                 $PDNSUTIL --config-dir=. --config-name=lmdb2 create-secondary-zone catalog.invalid 127.0.0.1:$port
                 $PDNSUTIL --config-dir=. --config-name=lmdb2 set-kind catalog.invalid consumer
 
index 9fbb87d32ca0073de003c1da77a8e58a8590ba3e..305f46bb09a18ea913ebc902dd05d4ac3ca0e61a 100644 (file)
@@ -22,6 +22,7 @@ No keys for zone '.'.
 ..myroot
 2.0.192.in-addr.arpa
 catalog.invalid
+catalog2.invalid
 cdnskey-cds-test.com
 cryptokeys.org
 delegated.dnssec-parent.com
diff --git a/regression-tests/zones/catalog2.invalid b/regression-tests/zones/catalog2.invalid
new file mode 100644 (file)
index 0000000..d661bf4
--- /dev/null
@@ -0,0 +1,10 @@
+$TTL 3600
+$ORIGIN catalog2.invalid.
+@              IN      SOA     ns1.zone.invalid. hostmaster.zone.invalid. (  1
+                       1M ; refresh
+                       30S ; retry
+                       1W ; expire
+                       1D ; default_ttl
+                       )
+
+@              IN      NS      ns1.zone.invalid.