From 308e3f6ffdd5fa966b7cc6f5ca721ef6c1d4b4fc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pavel=20K=C3=A1cha?= <ph@cesnet.cz>
Date: Mon, 13 Jun 2016 18:11:19 +0200
Subject: [PATCH] PlainAuthenticator (forego X509 for testing) (thx to Radko
 Krkos)

---
 warden3/warden_server/README           |  11 ++-
 warden3/warden_server/warden_server.py | 115 +++++++++++--------------
 2 files changed, 57 insertions(+), 69 deletions(-)

diff --git a/warden3/warden_server/README b/warden3/warden_server/README
index 92a0180..104fb61 100644
--- a/warden3/warden_server/README
+++ b/warden3/warden_server/README
@@ -37,7 +37,7 @@ B. Dependencies
  1. Platform
 
     Python 2.7+
-	Apache 2.2
+	Apache 2.2/2.4
 	mod_wsgi 3.3+
 
  2. Python modules
@@ -123,7 +123,7 @@ particular implementation object of the aspect, for example type of logger
 
 		Log: FileLogger, SysLogger
 		DB: MySQL
-		Auth: X509Authenticator, NoAuthenticator
+		Auth: X509Authenticator, PlainAuthenticator
 		Validator: JSONSchemaValidator, NoValidator
 		Handler: WardenHandler
 
@@ -142,12 +142,15 @@ object from particular section list is used ("FileLogger" for example).
       facility: syslog facility, defaults to "daemon"
       level: least log level (CRITICAL, ERROR, WARNING, INFO, DEBUG)
 
-   NoAuthenticator: forego authentication, for debug purposes
-
    X509Authenticator: authenticate based on certificate chain validation,
        hostname corresponding with certificate CN or SubjectAltName and
        optionally shared secret
 
+   PlainAuthenticator: authenticate based on client name or shared secret, usable
+      over plain HTTP connection or HTTPS without client certificate - note that
+      this pretty much spoils security, and is meant only for testing and
+      debugging purposes, NOT for production servers
+
    NoValidator: forego event JSON validation, for debug purposes
 
    JSONSchemaValidator: validate incoming events based on JSON schema file
diff --git a/warden3/warden_server/warden_server.py b/warden3/warden_server/warden_server.py
index 462f70f..3f29de1 100755
--- a/warden3/warden_server/warden_server.py
+++ b/warden3/warden_server/warden_server.py
@@ -272,44 +272,63 @@ class ObjectReq(Object):
 
 
 
-class NoAuthenticator(ObjectReq):
+class PlainAuthenticator(ObjectReq):
 
-    def __init__(self, req):
+    def __init__(self, req, db):
         ObjectReq.__init__(self, req)
+        self.db = db
 
 
-    def shash(self, s):
-        """ Simple FNV1 hash for creating ids on the fly """
-        res = 2166136261
-        for c in s:
-            res =  0xFFFFFFFF & res * 16777619 ^ ord(c)
-        return res
+    def __str__(self):
+        return "%s(req=%s, db=%s)" % (type(self).__name__, type(self.req).__name__, type(self.db).__name__)
 
 
     def authenticate(self, env, args):
         name = args.get("client", [None])[0]
-        if name is None:
-            logging.error("NoAuthenticator: clients must authenticate by name, not secret")
+        secret =  args.get("secret", [None])[0]
+        hostnames = args.get("hostnames", [None])[0]
+
+        client = self.db.get_client_by_name(hostnames, name, secret)
+
+        if not client:
+            logging.info("authenticate: client not found by name: \"%s\", secret: %s, hostnames: %s" % (
+                name, secret, str(hostnames)))
             return None
 
-        return Client(self.shash(name), None, None, None, name, None, 1, None, 1, 1, 1, 0)
+        # Clients with 'secret' set must get authenticated by it.
+        # No secret turns secret auth off for this particular client.
+        if client.secret is not None and secret is None:
+            logging.info("authenticate: missing secret argument")
+            return None
 
+        logging.info("authenticate: %s" % str(client))
 
-    def authorize(self, env, client, path, method):
-        return (client is not None)
+        return client
 
 
+    def authorize(self, env, client, path, method):
+        if method.debug:
+            if not client.debug:
+                logging.info("authorize: failed, client does not have debug enabled")
+                return None
+            return client
 
-class X509Authenticator(NoAuthenticator):
+        if method.read:
+            if not client.read:
+                logging.info("authorize: failed, client does not have read enabled")
+                return None
+            return client
 
-    def __init__(self, req, db):
-        NoAuthenticator.__init__(self, req)
-        self.db = db
+        if method.write:
+            if not (client.write or client.test):
+                logging.info("authorize: failed, client is not allowed to write or test")
+                return None
+
+        return client
 
 
-    def __str__(self):
-        return "%s(req=%s, db=%s)" % (type(self).__name__, type(self.req).__name__, type(self.db).__name__)
 
+class X509Authenticator(PlainAuthenticator):
 
     def get_cert_dns_names(self, pem):
 
@@ -330,7 +349,7 @@ class X509Authenticator(NoAuthenticator):
         return [firstcommon] + list(set(altnames+commons) - set([firstcommon]))
 
 
-    def authenticate (self, env, args):
+    def authenticate(self, env, args):
         try:
             cert_names = self.get_cert_dns_names(env["SSL_CLIENT_CERT"])
         except:
@@ -338,46 +357,9 @@ class X509Authenticator(NoAuthenticator):
             exception.log(logging.getLogger())
             return None
 
-        name = args.get("client", [None])[0]
-        secret =  args.get("secret", [None])[0]
+        args["hostnames"] = [cert_names]
 
-        client = self.db.get_client_by_name(cert_names, name, secret)
-
-        if not client:
-            logging.info("authenticate: client not found by name: \"%s\", secret: %s, cert_names: %s" % (
-                name, secret, str(cert_names)))
-            return None
-        
-        # Clients with 'secret' set must get authenticated by it.
-        # No secret turns secret auth off for this particular client.
-        if client.secret is not None and secret is None:
-            logging.info("authenticate: missing secret argument")
-            return None
-
-        logging.info("authenticate: %s" % str(client))
-
-        return client
-
-
-    def authorize(self, env, client, path, method):
-        if method.debug:
-            if not client.debug:
-                logging.info("authorize: failed, client does not have debug enabled")
-                return None
-            return client
-
-        if method.read:
-            if not client.read:
-                logging.info("authorize: failed, client does not have read enabled")
-                return None
-            return client
-
-        if method.write:
-            if not (client.write or client.test):
-                logging.info("authorize: failed, client is not allowed to write or test")
-                return None
-
-        return client
+        return PlainAuthenticator.authenticate(self, env, args)
         
 
 class NoValidator(ObjectReq):
@@ -510,7 +492,7 @@ class MySQL(ObjectReq):
         return "" if b else "NOT"
 
 
-    def get_client_by_name(self, cert_names, name=None, secret=None):
+    def get_client_by_name(self, cert_names=None, name=None, secret=None):
         query = ["SELECT * FROM clients WHERE valid = 1"]
         params = []
         if name:
@@ -519,8 +501,9 @@ class MySQL(ObjectReq):
         if secret:
             query.append(" AND secret = %s")
             params.append(secret)
-        query.append(" AND hostname IN (%s)" % self._get_comma_perc(cert_names))
-        params.extend(n.lower() for n in cert_names)
+        if cert_names:
+            query.append(" AND hostname IN (%s)" % self._get_comma_perc(cert_names))
+            params.extend(n.lower() for n in cert_names)
         rows = self.query("".join(query), params, commit=True).fetchall()
 
         if len(rows)>1:
@@ -860,6 +843,7 @@ class Server(ObjectReq):
             # These args are not for handler
             args.pop("client", None)
             args.pop("secret", None)
+            args.pop("hostnames", None)
 
             args = self.sanitize_args(path, method, args)
             result = method(**args)   # call requested method
@@ -1164,7 +1148,7 @@ def build_server(conf):
     section_def = {
         "log": ["FileLogger", "SysLogger"],
         "db": ["MySQL"],
-        "auth": ["X509Authenticator", "NoAuthenticator"],
+        "auth": ["X509Authenticator", "PlainAuthenticator"],
         "validator": ["JSONSchemaValidator", "NoValidator"],
         "handler": ["WardenHandler"],
         "server": ["Server"]
@@ -1183,8 +1167,9 @@ def build_server(conf):
             "facility": {"type": facility, "default": "daemon"},
             "level": {"type": loglevel, "default": "info"}
         },
-        "NoAuthenticator": {
-            "req": {"type": obj, "default": "req"}
+        "PlainAuthenticator": {
+            "req": {"type": obj, "default": "req"},
+            "db": {"type": obj, "default": "db"}
         },
         "X509Authenticator": {
             "req": {"type": obj, "default": "req"},
-- 
GitLab