]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
auth: add options to DomainInfo
authorKees Monshouwer <mind04@monshouwer.org>
Sun, 26 Jun 2022 22:38:11 +0000 (00:38 +0200)
committermind04 <mind04@monshouwer.org>
Fri, 8 Jul 2022 10:31:19 +0000 (12:31 +0200)
18 files changed:
modules/gmysqlbackend/4.3.0_to_4.7.0_schema.mysql.sql [new file with mode: 0644]
modules/gmysqlbackend/gmysqlbackend.cc
modules/gmysqlbackend/schema.mysql.sql
modules/godbcbackend/4.3.0_to_4.7.0_schema.mssql.sql [new file with mode: 0644]
modules/godbcbackend/godbcbackend.cc
modules/godbcbackend/schema.mssql.sql
modules/gpgsqlbackend/4.3.0_to_4.7.0_schema.pgsql.sql [new file with mode: 0644]
modules/gpgsqlbackend/gpgsqlbackend.cc
modules/gpgsqlbackend/schema.pgsql.sql
modules/gsqlite3backend/4.3.1_to_4.7.0_schema.sqlite3.sql [new file with mode: 0644]
modules/gsqlite3backend/gsqlite3backend.cc
modules/gsqlite3backend/schema.sqlite3.sql
modules/lmdbbackend/lmdbbackend.cc
modules/lmdbbackend/lmdbbackend.hh
pdns/backends/gsql/gsqlbackend.cc
pdns/backends/gsql/gsqlbackend.hh
pdns/dnsbackend.hh
pdns/pdnsutil.cc

diff --git a/modules/gmysqlbackend/4.3.0_to_4.7.0_schema.mysql.sql b/modules/gmysqlbackend/4.3.0_to_4.7.0_schema.mysql.sql
new file mode 100644 (file)
index 0000000..97fd3c4
--- /dev/null
@@ -0,0 +1 @@
+ALTER TABLE domains ADD options VARCHAR(64000) DEFAULT NULL AFTER notified_serial;
index fcea02024c6467ccb6545849d946da5c84cffaca..0b6073a6d30280b5937ab5106f1c352aac83d004 100644 (file)
@@ -101,7 +101,7 @@ public:
     declare(suffix, "remove-empty-non-terminals-from-zone-query", "remove all empty non-terminals from zone", "delete from records where domain_id=? and type is null");
     declare(suffix, "delete-empty-non-terminal-query", "delete empty non-terminal from zone", "delete from records where domain_id=? and name=? and type is null");
 
-    declare(suffix, "info-zone-query", "", "select id,name,master,last_check,notified_serial,type,account from domains where name=?");
+    declare(suffix, "info-zone-query", "", "select id,name,master,last_check,notified_serial,type,options,account from domains where name=?");
 
     declare(suffix, "info-all-slaves-query", "", "select id,name,master,last_check from domains where type='SLAVE'");
     declare(suffix, "supermaster-query", "", "select account from supermasters where ip=? and nameserver=?");
@@ -127,6 +127,7 @@ public:
 
     declare(suffix, "update-master-query", "", "update domains set master=? where name=?");
     declare(suffix, "update-kind-query", "", "update domains set type=? where name=?");
+    declare(suffix, "update-options-query", "", "update domains set options=? where name=?");
     declare(suffix, "update-account-query", "", "update domains set account=? where name=?");
     declare(suffix, "update-serial-query", "", "update domains set notified_serial=? where id=?");
     declare(suffix, "update-lastcheck-query", "", "update domains set last_check=? where id=?");
index c923035ba672389ee19a099156c20d5f967c70f8..6d7c17890d4c1d8904b4dfffc20f4ea84216ba1a 100644 (file)
@@ -5,6 +5,7 @@ CREATE TABLE domains (
   last_check            INT DEFAULT NULL,
   type                  VARCHAR(6) NOT NULL,
   notified_serial       INT UNSIGNED DEFAULT NULL,
+  options               VARCHAR(64000) DEFAULT NULL,
   account               VARCHAR(40) CHARACTER SET 'utf8' DEFAULT NULL,
   PRIMARY KEY (id)
 ) Engine=InnoDB CHARACTER SET 'latin1';
diff --git a/modules/godbcbackend/4.3.0_to_4.7.0_schema.mssql.sql b/modules/godbcbackend/4.3.0_to_4.7.0_schema.mssql.sql
new file mode 100644 (file)
index 0000000..8babd7a
--- /dev/null
@@ -0,0 +1 @@
+ALTER TABLE domains ADD COLUMN options VARCHAR(MAX) DEFAULT NULL;
index 87e7d4d828ab72a42326a3ad64932651c138e03d..055dedf6973bdd40db73fb08c2342a674ce703c0 100644 (file)
@@ -81,7 +81,7 @@ public:
     declare(suffix, "remove-empty-non-terminals-from-zone-query", "remove all empty non-terminals from zone", "delete from records where domain_id=? and type is null");
     declare(suffix, "delete-empty-non-terminal-query", "delete empty non-terminal from zone", "delete from records where domain_id=? and name=? and type is null");
 
-    declare(suffix, "info-zone-query", "", "select id,name,master,last_check,notified_serial,type,account from domains where name=?");
+    declare(suffix, "info-zone-query", "", "select id,name,master,last_check,notified_serial,type,options,account from domains where name=?");
 
     declare(suffix, "info-all-slaves-query", "", "select id,name,master,last_check from domains where type='SLAVE'");
     declare(suffix, "supermaster-query", "", "select account from supermasters where ip=? and nameserver=?");
@@ -107,6 +107,7 @@ public:
 
     declare(suffix, "update-master-query", "", "update domains set master=? where name=?");
     declare(suffix, "update-kind-query", "", "update domains set type=? where name=?");
+    declare(suffix, "update-options-query", "", "update domains set options=? where name=?");
     declare(suffix, "update-account-query", "", "update domains set account=? where name=?");
     declare(suffix, "update-serial-query", "", "update domains set notified_serial=? where id=?");
     declare(suffix, "update-lastcheck-query", "", "update domains set last_check=? where id=?");
index d5922dc85289c2d96619ce2d9af321707c3d505d..1646d4e665dd4363fb6dc171ae9a8682fe0f2fb5 100644 (file)
@@ -5,6 +5,7 @@ CREATE TABLE domains (
   last_check            INT DEFAULT NULL,
   type                  VARCHAR(6) NOT NULL,
   notified_serial       INT DEFAULT NULL,
+  options               VARCHAR(MAX) DEFAULT NULL,
   account               VARCHAR(40) DEFAULT NULL,
   PRIMARY KEY (id)
 );
diff --git a/modules/gpgsqlbackend/4.3.0_to_4.7.0_schema.pgsql.sql b/modules/gpgsqlbackend/4.3.0_to_4.7.0_schema.pgsql.sql
new file mode 100644 (file)
index 0000000..ed9b6e2
--- /dev/null
@@ -0,0 +1,8 @@
+BEGIN;
+  ALTER TABLE domains ADD COLUMN options VARCHAR(65535) DEFAULT NULL;
+
+  ALTER TABLE domains ADD COLUMN account_new VARCHAR(40) DEFAULT NULL;
+  UPDATE domains SET account_new = account;
+  ALTER TABLE domains DROP COLUMN account;
+  ALTER TABLE domains RENAME COLUMN account_new TO account;
+COMMIT;
index 906fa8128225e29eb24fd182bef74e24954e3951..2f7598e93315480fcfe86d9ac72220b9781066e8 100644 (file)
@@ -108,7 +108,7 @@ public:
     declare(suffix, "remove-empty-non-terminals-from-zone-query", "remove all empty non-terminals from zone", "delete from records where domain_id=$1 and type is null");
     declare(suffix, "delete-empty-non-terminal-query", "delete empty non-terminal from zone", "delete from records where domain_id=$1 and name=$2 and type is null");
 
-    declare(suffix, "info-zone-query", "", "select id,name,master,last_check,notified_serial,type,account from domains where name=$1");
+    declare(suffix, "info-zone-query", "", "select id,name,master,last_check,notified_serial,type,options,account from domains where name=$1");
 
     declare(suffix, "info-all-slaves-query", "", "select id,name,master,last_check from domains where type='SLAVE'");
     declare(suffix, "supermaster-query", "", "select account from supermasters where ip=$1 and nameserver=$2");
@@ -134,6 +134,7 @@ public:
 
     declare(suffix, "update-master-query", "", "update domains set master=$1 where name=$2");
     declare(suffix, "update-kind-query", "", "update domains set type=$1 where name=$2");
+    declare(suffix, "update-options-query", "", "update domains set options=$1 where name=$2");
     declare(suffix, "update-account-query", "", "update domains set account=$1 where name=$2");
     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");
index 80735c2e6b41c396c11908da6686a30a89568bf7..29bb1793b5fa13247b418397dece5e4fc1fbe2df 100644 (file)
@@ -5,6 +5,7 @@ CREATE TABLE domains (
   last_check            INT DEFAULT NULL,
   type                  VARCHAR(6) NOT NULL,
   notified_serial       BIGINT DEFAULT NULL,
+  options               VARCHAR(65535) DEFAULT NULL;
   account               VARCHAR(40) DEFAULT NULL,
   CONSTRAINT c_lowercase_name CHECK (((name)::TEXT = LOWER((name)::TEXT)))
 );
diff --git a/modules/gsqlite3backend/4.3.1_to_4.7.0_schema.sqlite3.sql b/modules/gsqlite3backend/4.3.1_to_4.7.0_schema.sqlite3.sql
new file mode 100644 (file)
index 0000000..32374d5
--- /dev/null
@@ -0,0 +1,24 @@
+PRAGMA foreign_keys = 0;
+
+BEGIN TRANSACTION;
+  CREATE TABLE domains_temp (
+    id                    INTEGER PRIMARY KEY,
+    name                  VARCHAR(255) NOT NULL COLLATE NOCASE,
+    master                VARCHAR(128) DEFAULT NULL,
+    last_check            INTEGER DEFAULT NULL,
+    type                  VARCHAR(6) NOT NULL,
+    notified_serial       INTEGER DEFAULT NULL,
+    options               VARCHAR(65535) DEFAULT NULL,
+    account               VARCHAR(40) DEFAULT NULL
+  );
+
+  INSERT INTO domains_temp SELECT id,name,master,last_check,type,motified_serial,NULL,account FROM domains;
+  DROP TABLE domains;
+  ALTER TABLE domains_temp RENAME TO domains;
+
+  CREATE UNIQUE INDEX name_index ON domains(name);
+COMMIT;
+
+PRAGMA foreign_keys = 1;
+
+ANALYZE;
index fd50ddcb601487735c777a18d1ddbdf6b3fc5fea..55d9f387ac21bbf7ec6def9cec23cb13e5c52a0f 100644 (file)
@@ -94,7 +94,7 @@ public:
     declare(suffix, "remove-empty-non-terminals-from-zone-query", "remove all empty non-terminals from zone", "delete from records where domain_id=:domain_id and type is null");
     declare(suffix, "delete-empty-non-terminal-query", "delete empty non-terminal from zone", "delete from records where domain_id=:domain_id and name=:qname and type is null");
 
-    declare(suffix, "info-zone-query", "", "select id,name,master,last_check,notified_serial,type,account from domains where name=:domain");
+    declare(suffix, "info-zone-query", "", "select id,name,master,last_check,notified_serial,type,options,account from domains where name=:domain");
 
     declare(suffix, "info-all-slaves-query", "", "select id,name,master,last_check from domains where type='SLAVE'");
     declare(suffix, "supermaster-query", "", "select account from supermasters where ip=:ip and nameserver=:nameserver");
@@ -120,6 +120,7 @@ public:
 
     declare(suffix, "update-master-query", "", "update domains set master=:master where name=:domain");
     declare(suffix, "update-kind-query", "", "update domains set type=:kind where name=:domain");
+    declare(suffix, "update-options-query", "", "update domains set options=:options where name=:domain");
     declare(suffix, "update-account-query", "", "update domains set account=:account where name=:domain");
     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");
index 08755211855fe5be610fd3463a36fa81574445f3..506732075554dbaabacd7bab6723d1789af3442d 100644 (file)
@@ -6,6 +6,7 @@ CREATE TABLE domains (
   master                VARCHAR(128) DEFAULT NULL,
   last_check            INTEGER DEFAULT NULL,
   type                  VARCHAR(6) NOT NULL,
+  options               VARCHAR(65535) DEFAULT NULL,
   notified_serial       INTEGER DEFAULT NULL,
   account               VARCHAR(40) DEFAULT NULL
 );
index bab19269634a50ec993fef43cec4341a2d017c3f..ae620a295e4ed92a45de5775688b26ec1c4edd5c 100644 (file)
@@ -53,6 +53,7 @@
 
 // List the class version here. Default is 0
 BOOST_CLASS_VERSION(LMDBBackend::KeyDataDB, 1)
+BOOST_CLASS_VERSION(DomainInfo, 1)
 
 static bool s_first = true;
 static int s_shards = 0;
@@ -191,7 +192,7 @@ namespace serialization
   }
 
   template <class Archive>
-  void serialize(Archive& ar, DomainInfo& g, const unsigned int version)
+  void save(Archive& ar, const DomainInfo& g, const unsigned int version)
   {
     ar& g.zone;
     ar& g.last_check;
@@ -200,6 +201,25 @@ namespace serialization
     ar& g.id;
     ar& g.notified_serial;
     ar& g.kind;
+    ar& g.options;
+  }
+
+  template <class Archive>
+  void load(Archive& ar, DomainInfo& g, const unsigned int version)
+  {
+    ar& g.zone;
+    ar& g.last_check;
+    ar& g.account;
+    ar& g.masters;
+    ar& g.id;
+    ar& g.notified_serial;
+    ar& g.kind;
+    if (version >= 1) {
+      ar& g.options;
+    }
+    else {
+      g.options.clear();
+    }
   }
 
   template <class Archive>
@@ -240,6 +260,7 @@ namespace serialization
 BOOST_SERIALIZATION_SPLIT_FREE(DNSName);
 BOOST_SERIALIZATION_SPLIT_FREE(QType);
 BOOST_SERIALIZATION_SPLIT_FREE(LMDBBackend::KeyDataDB);
+BOOST_SERIALIZATION_SPLIT_FREE(DomainInfo);
 BOOST_IS_BITWISE_SERIALIZABLE(ComboAddress);
 
 template <>
@@ -953,6 +974,13 @@ bool LMDBBackend::setKind(const DNSName& domain, const DomainInfo::DomainKind ki
   });
 }
 
+bool LMDBBackend::setOptions(const DNSName& domain, const std::string& options)
+{
+  return genChangeDomain(domain, [options](DomainInfo& di) {
+    di.options = options;
+  });
+}
+
 bool LMDBBackend::setAccount(const DNSName& domain, const std::string& account)
 {
   return genChangeDomain(domain, [account](DomainInfo& di) {
index 99244a9ef86f626ed5e5dede5f1b9e6a8bc63573..70f9e54922d2813220d86b9a33ffc3ac2e6a4148 100644 (file)
@@ -106,6 +106,7 @@ public:
   void setStale(uint32_t domain_id) override;
   void setFresh(uint32_t domain_id) override;
   void setNotified(uint32_t id, uint32_t serial) override;
+  bool setOptions(const DNSName& domain, const std::string& options) override;
   bool setAccount(const DNSName& domain, const std::string& account) override;
   bool deleteDomain(const DNSName& domain) override;
 
index ab8ef9f5b074dfecfc7f8d9641184d7eb1edbb85..0f4241c733dc714256c0bbc65663fac4c37862c4 100644 (file)
@@ -79,6 +79,7 @@ GSQLBackend::GSQLBackend(const string &mode, const string &suffix)
   d_UpdateKindOfZoneQuery=getArg("update-kind-query");
   d_UpdateSerialOfZoneQuery=getArg("update-serial-query");
   d_UpdateLastCheckOfZoneQuery=getArg("update-lastcheck-query");
+  d_UpdateOptionsOfZoneQuery = getArg("update-options-query");
   d_UpdateAccountOfZoneQuery=getArg("update-account-query");
   d_InfoOfAllMasterDomainsQuery=getArg("info-all-master-query");
   d_DeleteDomainQuery=getArg("delete-domain-query");
@@ -152,6 +153,7 @@ GSQLBackend::GSQLBackend(const string &mode, const string &suffix)
   d_UpdateKindOfZoneQuery_stmt = nullptr;
   d_UpdateSerialOfZoneQuery_stmt = nullptr;
   d_UpdateLastCheckOfZoneQuery_stmt = nullptr;
+  d_UpdateOptionsOfZoneQuery_stmt = nullptr;
   d_UpdateAccountOfZoneQuery_stmt = nullptr;
   d_InfoOfAllMasterDomainsQuery_stmt = nullptr;
   d_DeleteDomainQuery_stmt = nullptr;
@@ -275,6 +277,25 @@ bool GSQLBackend::setKind(const DNSName &domain, const DomainInfo::DomainKind ki
   return true;
 }
 
+bool GSQLBackend::setOptions(const DNSName& domain, const string& options)
+{
+  try {
+    reconnectIfNeeded();
+
+    // clang-format off
+    d_UpdateOptionsOfZoneQuery_stmt->
+      bind("options", options)->
+      bind("domain", domain)->
+      execute()->
+      reset();
+    // clang-format on
+  }
+  catch (SSqlException& e) {
+    throw PDNSException("GSQLBackend unable to set options of domain '" + domain.toLogString() + "' to '" + options + "': " + e.txtReason());
+  }
+  return true;
+}
+
 bool GSQLBackend::setAccount(const DNSName &domain, const string &account)
 {
   try {
@@ -313,7 +334,7 @@ bool GSQLBackend::getDomainInfo(const DNSName &domain, DomainInfo &di, bool getS
   if(!numanswers)
     return false;
 
-  ASSERT_ROW_COLUMNS("info-zone-query", d_result[0], 7);
+  ASSERT_ROW_COLUMNS("info-zone-query", d_result[0], 8);
 
   pdns::checked_stoi_into(di.id, d_result[0][0]);
   try {
@@ -322,7 +343,8 @@ bool GSQLBackend::getDomainInfo(const DNSName &domain, DomainInfo &di, bool getS
     return false;
   }
   string type=d_result[0][5];
-  di.account=d_result[0][6];
+  di.options = d_result[0][6];
+  di.account = d_result[0][7];
   di.kind = DomainInfo::stringToKind(type);
 
   vector<string> masters;
index 365fd69a8a5e4b086b75eaad1213e7bbd4b238a6..dbe02aac6105a250cd911a1b221b069730626865 100644 (file)
@@ -74,6 +74,7 @@ protected:
       d_InsertEmptyNonTerminalOrderQuery_stmt = d_db->prepare(d_InsertEmptyNonTerminalOrderQuery, 4);
       d_UpdateMasterOfZoneQuery_stmt = d_db->prepare(d_UpdateMasterOfZoneQuery, 2);
       d_UpdateKindOfZoneQuery_stmt = d_db->prepare(d_UpdateKindOfZoneQuery, 2);
+      d_UpdateOptionsOfZoneQuery_stmt = d_db->prepare(d_UpdateOptionsOfZoneQuery, 2);
       d_UpdateAccountOfZoneQuery_stmt = d_db->prepare(d_UpdateAccountOfZoneQuery, 2);
       d_UpdateSerialOfZoneQuery_stmt = d_db->prepare(d_UpdateSerialOfZoneQuery, 2);
       d_UpdateLastCheckOfZoneQuery_stmt = d_db->prepare(d_UpdateLastCheckOfZoneQuery, 2);
@@ -140,6 +141,7 @@ protected:
     d_InsertEmptyNonTerminalOrderQuery_stmt.reset();
     d_UpdateMasterOfZoneQuery_stmt.reset();
     d_UpdateKindOfZoneQuery_stmt.reset();
+    d_UpdateOptionsOfZoneQuery_stmt.reset();
     d_UpdateAccountOfZoneQuery_stmt.reset();
     d_UpdateSerialOfZoneQuery_stmt.reset();
     d_UpdateLastCheckOfZoneQuery_stmt.reset();
@@ -212,6 +214,7 @@ public:
   void setNotified(uint32_t domain_id, uint32_t serial) override;
   bool setMasters(const DNSName &domain, const vector<ComboAddress> &masters) override;
   bool setKind(const DNSName &domain, const DomainInfo::DomainKind kind) override;
+  bool setOptions(const DNSName& domain, const string& options) override;
   bool setAccount(const DNSName &domain, const string &account) override;
 
   bool getBeforeAndAfterNamesAbsolute(uint32_t id, const DNSName& qname, DNSName& unhashed, DNSName& before, DNSName& after) override;
@@ -303,6 +306,7 @@ private:
   string d_InsertEmptyNonTerminalOrderQuery;
   string d_UpdateMasterOfZoneQuery;
   string d_UpdateKindOfZoneQuery;
+  string d_UpdateOptionsOfZoneQuery;
   string d_UpdateAccountOfZoneQuery;
   string d_UpdateSerialOfZoneQuery;
   string d_UpdateLastCheckOfZoneQuery;
@@ -376,6 +380,7 @@ private:
   unique_ptr<SSqlStatement> d_InsertEmptyNonTerminalOrderQuery_stmt;
   unique_ptr<SSqlStatement> d_UpdateMasterOfZoneQuery_stmt;
   unique_ptr<SSqlStatement> d_UpdateKindOfZoneQuery_stmt;
+  unique_ptr<SSqlStatement> d_UpdateOptionsOfZoneQuery_stmt;
   unique_ptr<SSqlStatement> d_UpdateAccountOfZoneQuery_stmt;
   unique_ptr<SSqlStatement> d_UpdateSerialOfZoneQuery_stmt;
   unique_ptr<SSqlStatement> d_UpdateLastCheckOfZoneQuery_stmt;
index 84689c8087894132db7af4030c00dcb66bde79a4..a73d4438df2bf1f3de1c3cd3cecc717c8d340fd5 100644 (file)
@@ -50,6 +50,7 @@ struct DomainInfo
 
   DNSName zone;
   time_t last_check;
+  string options;
   string account;
   vector<ComboAddress> masters; 
   DNSBackend *backend;
@@ -349,6 +350,12 @@ public:
     return false;
   }
 
+  //! Called when the options of a domain should be changed
+  virtual bool setOptions(const DNSName& domain, const string& options)
+  {
+    return false;
+  }
+
   //! Called when the Account of a domain should be changed
   virtual bool setAccount(const DNSName &domain, const string &account)
   {
index 6e7066b784a06c1166ee46da9a6daf58846d99b8..75826d177434f0b85ae64aba7ccf956e6c8d2022 100644 (file)
@@ -15,6 +15,7 @@
 #include <boost/program_options.hpp>
 #include <boost/assign/std/vector.hpp>
 #include <boost/assign/list_of.hpp>
+#include "json11.hpp"
 #include "tsigutils.hh"
 #include "dnsbackend.hh"
 #include "ueberbackend.hh"
@@ -1858,6 +1859,22 @@ static bool disableDNSSECOnZone(DNSSECKeeper& dk, const DNSName& zone)
   return ret;
 }
 
+static int setZoneOptions(const DNSName& zone, const string& options)
+{
+  UeberBackend B("default");
+  DomainInfo di;
+
+  if (!B.getDomainInfo(zone, di)) {
+    cerr << "No such zone " << zone << " in the database" << endl;
+    return EXIT_FAILURE;
+  }
+  if (!di.backend->setOptions(zone, options)) {
+    cerr << "Could not find backend willing to accept new zone configuration" << endl;
+    return EXIT_FAILURE;
+  }
+  return EXIT_SUCCESS;
+}
+
 static int setZoneAccount(const DNSName& zone, const string &account)
 {
   UeberBackend B("default");
@@ -2101,6 +2118,10 @@ static bool showZone(DNSSECKeeper& dk, const DNSName& zone, bool exportDS = fals
       }
     }
   }
+  if (!di.options.empty()) {
+    cout << "Options:" << endl;
+    cout << di.options << endl;
+  }
   return true;
 }
 
@@ -2464,6 +2485,7 @@ try
     cout << "secure-all-zones [increase-serial] Secure all zones without keys" << endl;
     cout << "secure-zone ZONE [ZONE ..]         Add DNSSEC to zone ZONE" << endl;
     cout << "set-kind ZONE KIND                 Change the kind of ZONE to KIND (primary, secondary, native)" << endl;
+    cout << "set-options ZONE OPTIONS           Change the options of ZONE to OPTIONS" << endl;
     cout << "set-account ZONE ACCOUNT           Change the account (owner) of ZONE to ACCOUNT" << endl;
     cout << "set-nsec3 ZONE ['PARAMS' [narrow]] Enable NSEC3 with PARAMS. Optionally narrow" << endl;
     cout << "set-presigned ZONE                 Use presigned RRSIGs from storage" << endl;
@@ -3110,6 +3132,21 @@ try
     auto kind = DomainInfo::stringToKind(cmds.at(2));
     return setZoneKind(zone, kind);
   }
+  else if (cmds.at(0) == "set-options") {
+    if (cmds.size() != 3) {
+      cerr << "Syntax: pdnsutil set-options ZONE OPTIONS" << endl;
+      return 0;
+    }
+    // Verify json
+    std::string err;
+    json11::Json doc = json11::Json::parse(cmds.at(2), err);
+    if (doc.is_null()) {
+      cerr << "Parsing of JSON document failed:" << err << endl;
+      return EXIT_FAILURE;
+    }
+    DNSName zone(cmds.at(1));
+    return setZoneOptions(zone, cmds.at(2));
+  }
   else if (cmds.at(0) == "set-account") {
     if(cmds.size() != 3) {
       cerr<<"Syntax: pdnsutil set-account ZONE ACCOUNT"<<endl;