]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
Start kerberos services using docker for regression tests.
authorOtto Moerbeek <otto.moerbeek@open-xchange.com>
Tue, 4 Jan 2022 15:10:46 +0000 (16:10 +0100)
committerOtto Moerbeek <otto.moerbeek@open-xchange.com>
Fri, 2 Sep 2022 12:22:48 +0000 (14:22 +0200)
regression-tests.auth-py/authtests.py
regression-tests.auth-py/kerberos-client/init-keytab.sh [new file with mode: 0755]
regression-tests.auth-py/kerberos-client/krb5.conf [new file with mode: 0755]
regression-tests.auth-py/kerberos-client/kt.keytab [new file with mode: 0644]
regression-tests.auth-py/kerberos-client/update-policy.lua [new file with mode: 0644]
regression-tests.auth-py/kerberos-server/Dockerfile [new file with mode: 0644]
regression-tests.auth-py/kerberos-server/docker-compose.yml [new file with mode: 0644]
regression-tests.auth-py/kerberos-server/kerberos-init.sh [new file with mode: 0755]
regression-tests.auth-py/runtests
regression-tests.auth-py/test_GSSTSIG.py [new file with mode: 0644]
tasks.py

index e7a4e03a3b13f64498fef31bde7247ab11e78050..06fc48b773e1ba26a00272a99bc3a74b4f00c44d 100644 (file)
@@ -171,6 +171,20 @@ options {
             if cls._zone_keys.get(zonename, None):
                 cls.secureZone(confdir, zonename, cls._zone_keys.get(zonename))
 
+    @classmethod
+    def waitForTCPSocket(cls, ipaddress, port):
+        for try_number in range(0, 100):
+            try:
+                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+                sock.settimeout(1.0)
+                sock.connect((ipaddress, port))
+                sock.close()
+                return
+            except Exception as err:
+                if err.errno != errno.ECONNREFUSED:
+                    print(f'Error occurred: {try_number} {err}', file=sys.stderr)
+            time.sleep(0.1)
+
     @classmethod
     def startAuth(cls, confdir, ipaddress):
 
@@ -187,18 +201,14 @@ options {
             cls._auths[ipaddress] = subprocess.Popen(authcmd, close_fds=True,
                                                      stdout=fdLog, stderr=fdLog,
                                                      env=cls._auth_env)
-
-        time.sleep(2)
+        cls.waitForTCPSocket(ipaddress, cls._authPort)
 
         if cls._auths[ipaddress].poll() is not None:
-            try:
-                cls._auths[ipaddress].kill()
-            except OSError as e:
-                if e.errno != errno.ESRCH:
-                    raise
-                with open(logFile, 'r') as fdLog:
-                    print(fdLog.read())
-            sys.exit(cls._auths[ipaddress].returncode)
+            print(f"\n*** startAuth log for {logFile} ***")
+            with open(logFile, 'r') as fdLog:
+                print(fdLog.read())
+            print(f"*** End startAuth log for {logFile} ***")
+            raise AssertionError('%s failed (%d)' % (authcmd, cls._auths[ipaddress].returncode))
 
     @classmethod
     def setUpSockets(cls):
@@ -239,23 +249,32 @@ options {
         cls.tearDownAuth()
 
     @classmethod
-    def tearDownAuth(cls):
-        if 'PDNSRECURSOR_FAST_TESTS' in os.environ:
-            delay = 0.1
-        else:
-            delay = 1.0
+    def killProcess(cls, p):
+        # Don't try to kill it if it's already dead
+        if p.poll() is not None:
+            return
+        try:
+            p.terminate()
+            for count in range(10):
+                x = p.poll()
+                if x is not None:
+                    break
+                time.sleep(0.1)
+            if x is None:
+                print("kill...", p, file=sys.stderr)
+                p.kill()
+                p.wait()
+        except OSError as e:
+            # There is a race-condition with the poll() and
+            # kill() statements, when the process is dead on the
+            # kill(), this is fine
+            if e.errno != errno.ESRCH:
+                raise
 
+    @classmethod
+    def tearDownAuth(cls):
         for _, auth in cls._auths.items():
-            try:
-                auth.terminate()
-                if auth.poll() is None:
-                    time.sleep(delay)
-                    if auth.poll() is None:
-                        auth.kill()
-                    auth.wait()
-            except OSError as e:
-                if e.errno != errno.ESRCH:
-                    raise
+            cls.killProcess(auth)
 
     @classmethod
     def sendUDPQuery(cls, query, timeout=2.0, decode=True, fwparams=dict()):
diff --git a/regression-tests.auth-py/kerberos-client/init-keytab.sh b/regression-tests.auth-py/kerberos-client/init-keytab.sh
new file mode 100755 (executable)
index 0000000..ad4f331
--- /dev/null
@@ -0,0 +1,9 @@
+echo commands to run:
+echo Passwords enterd shoudl match those in kerberos-server setup script
+echo rm -f kt.keytab
+echo ktutil
+echo add_entry -password -p testuser1@EXAMPLE.COM -k 1 -e aes256-cts-hmac-sha1-96
+echo add_entry -password -p testuser2@EXAMPLE.COM -k 1 -e aes256-cts-hmac-sha1-96
+echo add_entry -password -p DNS/ns1.example.net@EXAMPLE.COM -k 1 -e aes256-cts-hmac-sha1-96
+echo wkt kt.keytab
+echo quit
diff --git a/regression-tests.auth-py/kerberos-client/krb5.conf b/regression-tests.auth-py/kerberos-client/krb5.conf
new file mode 100755 (executable)
index 0000000..1d65c48
--- /dev/null
@@ -0,0 +1,9 @@
+[libdefaults]
+        default_realm = EXAMPLE.COM
+
+[realms]
+        EXAMPLE.COM = {
+                kdc = 127.0.0.1:1188
+                admin_server = 127.0.0.1:1749
+        }
+
diff --git a/regression-tests.auth-py/kerberos-client/kt.keytab b/regression-tests.auth-py/kerberos-client/kt.keytab
new file mode 100644 (file)
index 0000000..f70cdff
Binary files /dev/null and b/regression-tests.auth-py/kerberos-client/kt.keytab differ
diff --git a/regression-tests.auth-py/kerberos-client/update-policy.lua b/regression-tests.auth-py/kerberos-client/update-policy.lua
new file mode 100644 (file)
index 0000000..509927f
--- /dev/null
@@ -0,0 +1,4 @@
+function updatepolicy(arg)
+  princ = arg:getPeerPrincipal()
+  return princ == "testuser2@EXAMPLE.COM"
+end
diff --git a/regression-tests.auth-py/kerberos-server/Dockerfile b/regression-tests.auth-py/kerberos-server/Dockerfile
new file mode 100644 (file)
index 0000000..b2d3f42
--- /dev/null
@@ -0,0 +1,20 @@
+FROM debian:bullseye
+
+EXPOSE 749 88
+
+ENV DEBIAN_FRONTEND noninteractive
+# The -qq implies --yes
+RUN apt-get -qq update
+RUN apt-get -qq install locales krb5-kdc krb5-admin-server
+RUN apt-get -qq clean
+
+#RUN locale-gen "en_US.UTF-8"
+#RUN echo "LC_ALL=\"en_US.UTF-8\"" >> /etc/default/locale
+
+ENV REALM ${REALM:-EXAMPLE.COM}
+ENV SUPPORTED_ENCRYPTION_TYPES ${SUPPORTED_ENCRYPTION_TYPES:-aes256-cts-hmac-sha1-96:normal}
+ENV KADMIN_PRINCIPAL ${KADMIN_PRINCIPAL:-kadmin/admin}
+ENV KADMIN_PASSWORD ${KADMIN_PASSWORD:-MITiys4K5}
+
+COPY kerberos-init.sh /tmp/
+CMD /tmp/kerberos-init.sh
diff --git a/regression-tests.auth-py/kerberos-server/docker-compose.yml b/regression-tests.auth-py/kerberos-server/docker-compose.yml
new file mode 100644 (file)
index 0000000..8afa3f9
--- /dev/null
@@ -0,0 +1,10 @@
+version: "2"
+services:
+  kerberos:
+    build: .
+    ports:
+      - "1188:88"
+      - "1749:749"
+    volumes:
+      # This is needed otherwise there won't be enough entropy to generate a new kerberos realm
+      - /dev/urandom:/dev/random
diff --git a/regression-tests.auth-py/kerberos-server/kerberos-init.sh b/regression-tests.auth-py/kerberos-server/kerberos-init.sh
new file mode 100755 (executable)
index 0000000..34dca0c
--- /dev/null
@@ -0,0 +1,57 @@
+#!/bin/bash
+
+KADMIN_PRINCIPAL_FULL=$KADMIN_PRINCIPAL@$REALM
+
+echo "REALM: $REALM"
+echo "KADMIN_PRINCIPAL_FULL: $KADMIN_PRINCIPAL_FULL"
+echo "KADMIN_PASSWORD: $KADMIN_PASSWORD"
+echo ""
+
+KDC_KADMIN_SERVER=$(hostname -f)
+tee /etc/krb5.conf <<EOF
+[libdefaults]
+       default_realm = $REALM
+
+[realms]
+       $REALM = {
+               kdc_ports = 88,750
+               kadmind_port = 749
+               kdc = $KDC_KADMIN_SERVER
+               admin_server = $KDC_KADMIN_SERVER
+       }
+EOF
+echo ""
+
+tee /etc/krb5kdc/kdc.conf <<EOF
+[realms]
+       $REALM = {
+               acl_file = /etc/krb5kdc/kadm5.acl
+               max_renewable_life = 7d 0h 0m 0s
+               supported_enctypes = $SUPPORTED_ENCRYPTION_TYPES
+               default_principal_flags = +preauth
+       }
+EOF
+
+tee /etc/krb5kdc/kadm5.acl <<EOF
+$KADMIN_PRINCIPAL_FULL *
+EOF
+
+MASTER_PASSWORD=$(tr -cd '[:alnum:]' < /dev/urandom | fold -w30 | head -n1)
+# This command also starts the krb5-kdc and krb5-admin-server services
+krb5_newrealm <<EOF
+$MASTER_PASSWORD
+$MASTER_PASSWORD
+EOF
+
+kadmin.local -q "delete_principal -force $KADMIN_PRINCIPAL_FULL"
+kadmin.local -q "addprinc -pw $KADMIN_PASSWORD $KADMIN_PRINCIPAL_FULL"
+
+kadmin.local -q "delete_principal -force testuser1@$REALM"
+kadmin.local -q "delete_principal -force testuser2@$REALM"
+kadmin.local -q "delete_principal -force DNS/ns1.example.net@$REALM"
+kadmin.local -q "addprinc -pw pw1 testuser1@$REALM"
+kadmin.local -q "addprinc -pw pw2 testuser2@$REALM"
+kadmin.local -q "addprinc -pw pw3 DNS/ns1.example.net@$REALM"
+
+krb5kdc
+kadmind -nofork
index 064d0bc89fd9f68ec77a15bcaceb7ca5c80fdd74..805f2af8771bc7674d47ec25b7550604efce1c28 100755 (executable)
@@ -27,9 +27,20 @@ for bin in "$PDNS" "$PDNSUTIL"; do
   fi
 done
 
-set -e
 if [ "${PDNS_DEBUG}" = "YES" ]; then
   set -x
 fi
 
-nosetests --with-xunit $@
+ignore="-I test_GSSTSIG.py"
+if [ "${WITHKERBEROS}" = "YES" ]; then
+    ignore=""
+    (cd kerberos-server && docker-compose up --detach --build)
+fi
+
+nosetests --with-xunit $ignore $@
+ret=$?
+
+if [ "${WITHKERBEROS}" = "YES" ]; then
+    (cd kerberos-server && docker-compose stop || exit 0)
+fi
+exit $ret
diff --git a/regression-tests.auth-py/test_GSSTSIG.py b/regression-tests.auth-py/test_GSSTSIG.py
new file mode 100644 (file)
index 0000000..f93fd26
--- /dev/null
@@ -0,0 +1,135 @@
+#!/usr/bin/env python
+import dns
+import os
+import subprocess
+
+from authtests import AuthTest
+
+
+class GSSTSIGBase(AuthTest):
+    _config_template_default = """
+module-dir=../regression-tests/modules
+daemon=no
+socket-dir={confdir}
+cache-ttl=0
+negquery-cache-ttl=0
+query-cache-ttl=0
+log-dns-queries=yes
+log-dns-details=yes
+loglevel=9
+distributor-threads=1"""
+
+    _config_template = """
+launch=gsqlite3
+gsqlite3-database=configs/auth/powerdns.sqlite
+gsqlite3-pragma-foreign-keys=yes
+gsqlite3-dnssec=yes
+enable-gss-tsig=yes
+allow-dnsupdate-from=0.0.0.0/0
+dnsupdate=yes
+"""
+    _auth_env = {'KRB5_CONFIG' : './kerberos-client/krb5.conf',
+                 'KRB5_KTNAME' : './kerberos-client/kt.keytab'
+                 }
+
+    @classmethod
+    def setUpClass(cls):
+        super(GSSTSIGBase, cls).setUpClass()
+        os.system("$PDNSUTIL --config-dir=configs/auth delete-zone example.net")
+        os.system("$PDNSUTIL --config-dir=configs/auth delete-zone noacceptor.net")
+        os.system("$PDNSUTIL --config-dir=configs/auth delete-zone wrongacceptor.net")
+        os.system("$PDNSUTIL --config-dir=configs/auth create-zone example.net")
+        os.system("$PDNSUTIL --config-dir=configs/auth create-zone noacceptor.net")
+        os.system("$PDNSUTIL --config-dir=configs/auth create-zone wrongacceptor.net")
+
+        os.system("$PDNSUTIL --config-dir=configs/auth add-record example.net . SOA 3600 'ns1.example.net otto.example.net 2022010403 10800 3600 604800 3600'")
+        os.system("$PDNSUTIL --config-dir=configs/auth add-record noacceptor.net . SOA 3600 'ns1.noacceptor.net otto.example.net 2022010403 10800 3600 604800 3600'")
+        os.system("$PDNSUTIL --config-dir=configs/auth add-record wrongacceptor.net . SOA 3600 'ns1.wrongacceptor.net otto.example.net 2022010403 10800 3600 604800 3600'")
+
+        os.system("$PDNSUTIL --config-dir=configs/auth set-meta example.net GSS-ACCEPTOR-PRINCIPAL DNS/ns1.example.net@EXAMPLE.COM")
+        os.system("$PDNSUTIL --config-dir=configs/auth set-meta wrongacceptor.net GSS-ACCEPTOR-PRINCIPAL DNS/ns1.example.net@EXAMPLE.COM")
+        os.system("$PDNSUTIL --config-dir=configs/auth set-meta example.net TSIG-ALLOW-DNSUPDATE testuser1@EXAMPLE.COM")
+
+    def kinit(self, user):
+        ret = subprocess.run(["kinit", "-Vt", "./kerberos-client/kt.keytab", user], env=self._auth_env)
+        self.assertEqual(ret.returncode, 0)
+
+    def nsupdate(self, commands, expected=0):
+        full = "server 127.0.0.1 %s\n" % self._authPort
+        full += commands + "\nsend\nquit\n"
+        ret = subprocess.run(["nsupdate", "-g"], input=full, env=self._auth_env, capture_output=True, text=True)
+        self.assertEqual(ret.returncode, expected)
+
+    def checkInDB(self, zone, record):
+        ret = os.system("$PDNSUTIL --config-dir=configs/auth list-zone %s | egrep -q %s" % (zone, record))
+        self.assertEqual(ret, 0)
+
+    def checkNotInDB(self, zone, record):
+        ret = os.system("$PDNSUTIL --config-dir=configs/auth list-zone %s | fgrep -q %s" % (zone, record))
+        self.assertNotEqual(ret, 0)
+
+class TestBasicGSSTSIG(GSSTSIGBase):
+
+    _config_template = """
+launch=gsqlite3
+gsqlite3-database=configs/auth/powerdns.sqlite
+gsqlite3-pragma-foreign-keys=yes
+gsqlite3-dnssec=yes
+enable-gss-tsig=yes
+allow-dnsupdate-from=0.0.0.0/0
+dnsupdate=yes
+"""
+    def testAllowedUpdate(self):
+        self.checkNotInDB('example.net', 'inserted1.example.net')
+        self.kinit("testuser1")
+        self.nsupdate("add inserted1.example.net 10 A 1.2.3.1")
+        self.checkInDB('example.net', '^inserted1.example.net.*10.*IN.*A.*1.2.3.1$')
+
+    def testDisallowedUpdate(self):
+        self.kinit("testuser2")
+        self.nsupdate("add inserted2.example.net 10 A 1.2.3.2", 2)
+        self.checkNotInDB('example.net', 'inserted2.example.net')
+
+    def testNoAcceptor(self):
+        self.kinit("testuser1")
+        self.nsupdate("add inserted3.noacceptor.net 10 A 1.2.3.3", 2)
+        self.checkNotInDB('example.net', 'inserted3.example.net')
+
+    def testWrongAcceptor(self):
+        self.kinit("testuser1")
+        self.nsupdate("add inserted4.wrongacceptor.net 10 A 1.2.3.4", 2)
+        self.checkNotInDB('example.net', 'inserted4.example.net')
+
+class TestLuaGSSTSIG(GSSTSIGBase):
+
+    _config_template = """
+launch=gsqlite3
+gsqlite3-database=configs/auth/powerdns.sqlite
+gsqlite3-pragma-foreign-keys=yes
+gsqlite3-dnssec=yes
+enable-gss-tsig=yes
+allow-dnsupdate-from=0.0.0.0/0
+dnsupdate=yes
+lua-dnsupdate-policy-script=kerberos-client/update-policy.lua
+"""
+    def testDisallowedByLuaUpdate(self):
+        self.kinit("testuser1")
+        self.nsupdate("add inserted10.example.net 10 A 1.2.3.10", 0) # Lua deny is still a NOERROR
+        self.checkNotInDB('example.net', 'inserted10.example.net')
+
+    def testAllowedByLuaUpdate(self):
+        self.kinit("testuser2")
+        self.nsupdate("add inserted11.example.net 10 A 1.2.3.11")
+        self.checkInDB('example.net', '^inserted11.example.net.*10.*IN.*A.*1.2.3.11$')
+
+
+    def testNoAcceptor(self):
+        self.kinit("testuser1")
+        self.nsupdate("add inserted12.noacceptor.net 10 A 1.2.3.12", 2)
+        self.checkNotInDB('example.net', 'inserted12.example.net')
+
+    def testWrongAcceptor(self):
+        self.kinit("testuser1")
+        self.nsupdate("add inserted13.wrongacceptor.net 10 A 1.2.3.13", 2)
+        self.checkNotInDB('example.net', 'inserted13.example.net')
+
index 634b491b9106ddc81d2e4ee50be868243dd2308c..0a2b8e411960f60f76365cc91376d26f06d6b4ea 100644 (file)
--- a/tasks.py
+++ b/tasks.py
@@ -86,8 +86,10 @@ auth_test_deps = [   # FIXME: we should be generating some of these from shlibde
     'curl',
     'default-jre-headless',
     'dnsutils',
+    'docker-compose',
     'faketime',
     'gawk',
+    'krb5-user',
     'ldnsutils',
     'libboost-serialization1.71.0',
     'libcdb1',
@@ -499,7 +501,7 @@ def test_auth_backend(c, backend):
 
     if backend == 'authpy':
         with c.cd('regression-tests.auth-py'):
-            c.run(f'PDNS=/opt/pdns-auth/sbin/pdns_server PDNS2=/opt/pdns-auth/sbin/pdns_server SDIG=/opt/pdns-auth/bin/sdig NOTIFY=/opt/pdns-auth/bin/pdns_notify NSEC3DIG=/opt/pdns-auth/bin/nsec3dig SAXFR=/opt/pdns-auth/bin/saxfr ZONE2SQL=/opt/pdns-auth/bin/zone2sql ZONE2LDAP=/opt/pdns-auth/bin/zone2ldap ZONE2JSON=/opt/pdns-auth/bin/zone2json PDNSUTIL=/opt/pdns-auth/bin/pdnsutil PDNSCONTROL=/opt/pdns-auth/bin/pdns_control PDNSSERVER=/opt/pdns-auth/sbin/pdns_server SDIG=/opt/pdns-auth/bin/sdig GMYSQLHOST=127.0.0.1 GMYSQL2HOST=127.0.0.1 MYSQL_HOST="127.0.0.1" PGHOST="127.0.0.1" PGPORT="5432" ./runtests')
+            c.run(f'PDNS=/opt/pdns-auth/sbin/pdns_server PDNS2=/opt/pdns-auth/sbin/pdns_server SDIG=/opt/pdns-auth/bin/sdig NOTIFY=/opt/pdns-auth/bin/pdns_notify NSEC3DIG=/opt/pdns-auth/bin/nsec3dig SAXFR=/opt/pdns-auth/bin/saxfr ZONE2SQL=/opt/pdns-auth/bin/zone2sql ZONE2LDAP=/opt/pdns-auth/bin/zone2ldap ZONE2JSON=/opt/pdns-auth/bin/zone2json PDNSUTIL=/opt/pdns-auth/bin/pdnsutil PDNSCONTROL=/opt/pdns-auth/bin/pdns_control PDNSSERVER=/opt/pdns-auth/sbin/pdns_server SDIG=/opt/pdns-auth/bin/sdig GMYSQLHOST=127.0.0.1 GMYSQL2HOST=127.0.0.1 MYSQL_HOST="127.0.0.1" PGHOST="127.0.0.1" PGPORT="5432" WITHKERBEROS=YES ./runtests')
         return
 
     with c.cd('regression-tests'):