Forráskód Böngészése

Add LDAP authentication features

To be documented in the readme...
Daniel Gruno 5 éve
szülő
commit
87d4d772c0
2 módosított fájl, 98 hozzáadás és 5 törlés
  1. 20 5
      pypubsub.py
  2. 78 0
      pypubsub_ldap.py

+ 20 - 5
pypubsub.py

@@ -25,6 +25,7 @@ import yaml
 import netaddr
 import binascii
 import base64
+import pypubsub_ldap
 
 # Some consts
 PUBSUB_VERSION = '0.3.0'
@@ -34,6 +35,7 @@ PUBSUB_PAYLOAD_RECEIVED = "Payload received, thank you very much!\n"
 PUBSUB_NOT_ALLOWED = "You are not authorized to deliver payloads!\n"
 PUBSUB_BAD_PAYLOAD = "Bad payload type. Payloads must be JSON dictionary objects, {..}!\n"
 CONF = None
+LCONF = None
 ACL = None
 OLD_SCHOOLERS = ['svnwcsub', ]  # Old-school clients that use \0 terminators.
 
@@ -63,12 +65,8 @@ class Subscriber:
             if ua in request.headers.get('User-Agent', ''):
                 self.old_school = True
                 break
-        # Is there a basic auth in this request? If so, set up ACL
-        auth = request.headers.get('Authorization')
-        if auth:
-            self.set_acl(auth)
 
-    def set_acl(self, basic):
+    async def set_acl(self, basic):
         """ Sets the ACL if possible, based on Basic Auth """
         try:
             decoded = str(base64.decodebytes(bytes(basic.replace('Basic ', ''), 'utf-8')), 'utf-8')
@@ -85,6 +83,15 @@ class Subscriber:
                         if not isinstance(v, list):
                             raise AssertionError(f"ACL segment {k} for user {u} is not a list of topics!")
                     print(f"Client {u} successfully authenticated (and ACL is valid).")
+            elif LCONF:
+                groups = await pypubsub_ldap.get_groups(LCONF, u, p)
+                # Make sure each ACL segment is a list of topics
+                for k, v in CONF['clients']['ldap']['acl'].items():
+                    if not isinstance(v, list):
+                        raise AssertionError(f"ACL segment {k} for user {u} is not a list of topics!")
+                    if k in groups:
+                        print(f"Enabling ACL segment {k} for user {u}")
+                        self.acl[k] = v
         except binascii.Error as e:
             self.acl = {}
         except AssertionError as e:
@@ -199,6 +206,11 @@ async def handler(request):
         if request.version.major == 1 and request.version.minor == 0:
             return resp
         subscriber = Subscriber(resp, request)
+        # Is there a basic auth in this request? If so, set up ACL
+        auth = request.headers.get('Authorization')
+        if auth:
+            await subscriber.set_acl(auth)
+
         SUBSCRIBERS.append(subscriber)
         # We'll change the content type once we're ready
         # resp.content_type = 'application/vnd.apache-pubsub-stream'
@@ -238,6 +250,9 @@ async def main():
 
 if __name__ == '__main__':
     CONF = yaml.safe_load(open('pypubsub.yaml'))
+    if 'ldap' in CONF.get('clients', {}):
+        pypubsub_ldap.vet_settings(CONF['clients']['ldap'])
+        LCONF = CONF['clients']['ldap']
     ACL = {}
     try:
         ACL = yaml.safe_load(open('pypubsub_acl.yaml'))

+ 78 - 0
pypubsub_ldap.py

@@ -0,0 +1,78 @@
+#!/usr/bin/env python3
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+""" This is the LDAP component of PyPubSub """
+
+import ldap
+import asyncio
+
+LDAP_SCHEME = {
+    'uri': str,
+    'user_dn': str,
+    'base_scope': str,
+    'membership_patterns': list,
+    'acl': dict
+}
+
+
+def vet_settings(lconf):
+    """ Simple test to vet LDAP settings if present """
+    if lconf:
+        for k, v in LDAP_SCHEME.items():
+            if not isinstance(lconf.get(k), v):
+                raise Exception(f"LDAP configuration item {k} must be of type {v.__name__}!")
+        assert ldap.initialize(lconf['uri'])
+    print("==== LDAP configuration looks kosher, enabling LDAP authentication as fallback ====")
+
+
+async def get_groups(lconf, user, password):
+    """ Async fetching of groups an LDAP user belongs to """
+    bind_dn = lconf['user_dn'] % user
+
+    try:
+        client = ldap.initialize(lconf['uri'])
+        client.set_option(ldap.OPT_REFERRALS, 0)
+        client.set_option(ldap.OPT_TIMEOUT, 0)
+        rv = client.simple_bind(bind_dn, password)
+        while True:
+            res = client.result(rv, timeout=0)
+            if res and res != (None, None):
+                break
+            await asyncio.sleep(0.25)
+
+        groups = []
+        for role in lconf['membership_patterns']:
+            rv = client.search(lconf['base_scope'], ldap.SCOPE_SUBTREE, role % user, ['dn'])
+            while True:
+                res = client.result(rv, all=0, timeout=0)
+                if res:
+                    if res == (None, None):
+                        await asyncio.sleep(0.25)
+                    else:
+                        if not res[1]:
+                            break
+                        for tuples in res[1]:
+                            groups.append(tuples[0])
+                else:
+                    break
+        return groups
+
+    except Exception as e:
+        print(f"LDAP Exception for user {user}: {e}")
+        return []
+