from nameko.events import EventDispatcher, event_handler from nameko.rpc import rpc from configparser import ConfigParser from ldap3 import Server, ServerPool, Connection, ObjectDef, AttrDef, Reader, Writer from datetime import datetime # For testing from random import randint # Read the config in the nameko.conf config = ConfigParser() config.read('nameko.conf') # Sanity check for config try: mult_server = int(config['LDAP']['SERVERMULTIPLE']) # SERVERMULTIPLE is set to something not a number except: exit("[LDAP] SERVERMULTIPLE has to be an integer >= 1") # less than one server is not a sensible option if mult_server < 1: exit("[LDAP] SERVERMULTIPLE has to be an integer >= 1") # Function to setup the server or serverpool def ldapservers(): # Just one server, no need for a pool if mult_server == 1: ldapserver = Server(config['LDAP']['LDAPSERVER1'], use_ssl=True) return ldapserver # Multiple servers, set up a pool else: ldapserver = ServerPool(None) for x in range(1, (mult_server+1)): ins = Server(config['LDAP']['LDAPSERVER' + str(x)], use_ssl=True) ldapserver.add(ins) return ldapserver # Since there's no reason why someone in ou=users shouldn't use the service, # here's the helper function to check whether an uid is in ou=customers or # ou=users # returns the full dn def user_or_customer(uid): server = ldapservers() conn = Connection(server, config['LDAP']['LDAPMANAGER'], config.get('LDAP','LDAPMANAGERPASSWORD', raw=True)) conn.bind() search_customers = conn.search('ou=customers,dc=ungleich,dc=ch', '(%s)' % uid) if search_customers: conn.unbind() return '%s,ou=customers,dc=ungleich,dc=ch' % uid search_users = conn.search('ou=users,dc=ungleich,dc=ch', '(%s)' % uid) if search_users: conn.unbind() return '%s,ou=users,dc=ungleich,dc=ch' % uid conn.unbind() return False # Get the objectclasses def objclasses(rdn, uid, connection): # search for objectClasses connection.search(rdn, '(%s)' % uid, attributes=['objectClass']) objclass = [] # get the relevant data tmp = connection.entries[0]['objectClass'] # This one sets up the array for y in tmp: objclass.append(y) # return the array containing the objectClasses, like ['inetOrgPerson', 'posixAccount', 'ldapPublicKey'] return objclass # checks if a user already exists in the LDAP class UserLookUp(object): name = "userlookup" dispatch = EventDispatcher() @rpc def lookup(self, user): # Setup the search parameter and connect to LDAP LDAP_UID = 'uid=%s' % user server = ldapservers() conn = Connection(server, config['LDAP']['LDAPMANAGER'], config.get('LDAP','LDAPMANAGERPASSWORD', raw=True)) conn.bind() # Strange result. It keeps complaining LDAP_UID not set if I try to directly # substitute x and y to the if # Searches for user in ou=customers and ou=users x = conn.search('ou=customers,dc=ungleich,dc=ch', '(%s)' % LDAP_UID) y = conn.search('ou=users,dc=ungleich,dc=ch', '(%s)' % LDAP_UID) if x or y: # return conn.entries[0] for first search result since we can assume uid is unique self.dispatch('ldap', '%s [Info: UserLookUp] Searched for %s and found it\n' % (datetime.now(), LDAP_UID) ) conn.unbind() # return True since the user is already in LDAP return True # User not in LDAP, so just close it down, write the log and return False else: conn.unbind() self.dispatch('ldap', '%s [Info: UserLookUp] Searched for %s and not found it.\n' % (datetime.now(), LDAP_UID) ) return False # Create a user in the LDAP. Assumes the checks are already in place for existing users class CreateUser(object): name = "createuser" dispatch = EventDispatcher() @rpc def create_user(self, user, password, firstname, lastname, email): # Creates a user with some basic data server = ldapservers() conn = Connection(server, config['LDAP']['LDAPMANAGER'], config.get('LDAP','LDAPMANAGERPASSWORD', raw=True)) if not conn.bind(): self.dispatch('ldap', '%s [Error CreateUser] Could not connect to LDAPserver\n' % datetime.now() ) return "Could not connect to LDAP Server." # set objectClasses for the new user obj_new_user = ObjectDef(['inetOrgPerson', 'posixAccount', 'ldapPublicKey'], conn) w = Writer(conn, obj_new_user) dn = 'uid=%s,ou=users,dc=ungleich,dc=ch' % user w.new(dn) # Filling in some of the data # required attributes are sn, cn, homeDirectory, uid (already handled by dn), uidNumber, gidNumber w[0].givenName = firstname w[0].sn = lastname w[0].cn = firstname + " " + lastname w[0].mail = email w[0].userPassword = password w[0].homeDirectory = '/home/%s' % user # Set uidNumber as last used uidNumber+1 w[0].uidNumber = self.get_new_uid_number(conn) # gidNumber for users created by userservice, nice and clear w[0].gidNumber = 10004 if not w.commit(): conn.unbind() self.dispatch('ldap', '%s [Error CreateUser] Could not write new user %s to LDAP DB\n' % (datetime.now(), dn) ) return "Couldn't write data to the LDAP Server." conn.unbind() self.dispatch('ldap', '%s [Info CreateUser] %s created.\n' % (datetime.now(), dn) ) return True # Function to get the next uid number. Not elegant, but LAM does it too and didn't really find anything # nicer. The sorted() seems to be quite efficient, so it shouldn't take too long even on larger arrays def get_new_uid_number(self, conn): conn.search('dc=ungleich,dc=ch', '(&(objectClass=posixAccount)(uidNumber=*))', attributes = [ 'uidNumber' ]) newuid = 0 uidlist = [] for c in conn.response: uidlist.append(c['attributes']['uidNumber']) # New uid is highest old uidnumber plus one newuid = (sorted(uidlist)[len(uidlist)-1] + 1) return newuid # Returns some basic data from an user class GetUserData(object): name = "getuserdata" dispatch = EventDispatcher() @rpc def get_data(self, user): # Setup the search parameter and connect to LDAP LDAP_UID = 'uid=%s' % user server = ldapservers() conn = Connection(server, config['LDAP']['LDAPMANAGER'], config.get('LDAP', 'LDAPMANAGERPASSWORD', raw=True)) conn.bind() if not conn.bound: self.dispatch('ldap', '%s [Error GetUserData] Could not connect to LDAP server.\n' % datetime.now() ) return ("error", "Could not connect to LDAP server.", "", "") rdn = user_or_customer(LDAP_UID) if rdn == False: conn.unbind() self.dispatch('ldap', '%s [Info GetUserData] Could not find user %s\n' % (datetime.now(), LDAP_UID) ) return ("error", "Could not find the user.", "", "") # Workaround because not all users have the same objectClasses objclass = objclasses(rdn, LDAP_UID, conn) obj = ObjectDef(objclass, conn) # The Reader gets the data for the user r = Reader(conn, obj, rdn) r.search() # Since the DN is basically confirmed by user_or_customer() it shouldn't throw an exception, but better check try: x = r[0].sn except: conn.unbind() self.dispatch('ldap', '%s [Error GetUserData] Could not open Reader for %s\n' % (datetime.now(), rdn) ) return ("error", "Could not read data for user.", "", "") # Putting the results into strings and then clean it up a bit if some attribute is not set in LDAP (firstname, lastname, email) = (str(r[0].givenName), str(r[0].sn), str(r[0].mail)) if firstname == '[]': firstname = 'No firstname given' if lastname == '[]': lastname = 'No lastname given' if email == '[]': email = 'No email given' conn.unbind() self.dispatch('ldap', '%s [Info GetUserData] Got data for %s Firstname: %s Lastname: %s Email: %s\n' % (datetime.now(), rdn, firstname, lastname, email) ) return ("OK", firstname, lastname, email) # change some (firstname, lastname, email) data for the user class ChangeUserData(object): name = "changeuserdata" dispatch = EventDispatcher() @rpc def change_data(self, user, firstname, lastname, email): LDAP_UID = 'uid=%s' % user server = ldapservers() # Establish connection with a user who can change the data conn = Connection(server, config['LDAP']['LDAPMANAGER'], config.get('LDAP', 'LDAPMANAGERPASSWORD', raw=True)) if not conn.bind(): self.dispatch('ldap', '%s [Error ChangeUserData] Could not connect to LDAP server.\n' % datetime.now() ) return "Could not connect to LDAP server." # get the DN of the user rdn = user_or_customer(LDAP_UID) if rdn == False: conn.unbind() self.dispatch('ldap', '%s [Info ChangeUserData] User with %s not found.\n' % (datetime.now(), LDAP_UID) ) return "Could not find user." # Fix because not every user has the same objectClasses objclass = objclasses(rdn, LDAP_UID, conn) # Set up a reader for the user obj = ObjectDef(objclass, conn) r = Reader(conn, obj, rdn) r.search() # Again, user_or_customer() should prevent it from throwing an exception because it's a confirmed user try: x = r[0].sn except: conn.unbind() self.dispatch('ldap', '%s [Error ChangeUserData] Could not open Reader for %s\n' % (datetime.now(), rdn) ) return "Could not open the data of user." # Opens a Writer instance prefilled with the old data # We could check if something has changed, but since the form takes the old data as standard values, let's # just update the relevant attributes w = Writer.from_cursor(r) w[0].sn = lastname w[0].cn = firstname + " " + lastname w[0].givenName = firstname w[0].mail = email # check if the data is written if not w.commit(): conn.unbind() self.dispatch('ldap', '%s [Error ChangeUserData] Could not write changes for %s\n' % (datetime.now(), rdn) ) return "Could not write changes for user." conn.unbind() self.dispatch('ldap', '%s [Info ChangeUserData] Changed data for %s Firstname: %s Lastname: %s Email: %s\n' % (datetime.now(), rdn, firstname, lastname, email) ) return True # change the password for the user class ChangePassword(object): name = "changepassword" dispatch = EventDispatcher() @rpc def change_password(self, user, newpassword): LDAP_UID = 'uid=%s' % user server = ldapservers() conn = Connection(server, config['LDAP']['LDAPMANAGER'], config.get('LDAP', 'LDAPMANAGERPASSWORD', raw=True)) if not conn.bind(): self.dispatch('ldap', '%s [Error ChangePassword] Could not connect to LDAP server.\n' % datetime.now() ) return "Could not connect to LDAP server." # check if uid=user is in either ou=customers or ou=users rdn = user_or_customer(LDAP_UID) if rdn == False: conn.unbind() self.dispatch('ldap', '%s [Error ChangePassword] Could not find user %s\n' % (datetime.now(), LDAP_UID) ) return "Could not find the user." # Plus not everyone has the same objectClasses, so workaround objclass = objclasses(rdn, LDAP_UID, conn) obj = ObjectDef(objclass, conn) # Set up a Reader for the DN r = Reader(conn, obj, rdn) r.search() # Shouldn't throw an exception, since the user is confirmed to be there try: x = r[0].sn except: conn.unbind() self.dispatch('ldap', '%s [Error ChangePassword] Could not open Reader for %s\n' % (datetime.now(), rdn) ) return "Could not open the data for the user." # Set up the writer and overwrite the attribute with the new password w = Writer.from_cursor(r) w[0].userPassword = newpassword # Check to see if the change has gone through if not w.commit(): conn.unbind() self.dispatch('ldap', '%s [Error ChangePassword] Could not write data for %s\n' % (datetime.now(), rdn) ) return "Could not write data for the user." conn.unbind() self.dispatch('ldap', '%s [Info ChangePassword] Password changed for %s\n' % (datetime.now(), rdn) ) return True # Deletes a user from LDAP class DeleteUser(object): name = "deleteuser" dispatch = EventDispatcher() @rpc def delete_user(self, user): LDAP_UID = 'uid=%s' % user server = ldapservers() conn = Connection(server, config['LDAP']['LDAPMANAGER'], config.get('LDAP', 'LDAPMANAGERPASSWORD', raw=True)) conn.bind() if not conn.bound: self.dispatch('ldap', '%s [Error DeleteUser] Could not connect to LDAP server.\n' % datetime.now() ) return "Could not connect to LDAP server." # again, check whether the uid= is in ou=users or ou=customers dn = user_or_customer(LDAP_UID) if dn == False: conn.unbind() self.dispatch('ldap', '%s [Error DeleteUser] Could not find the user %s\n' % (datetime.now(), LDAP_UID) ) return "Could not find the user." # Check if the delete was successfull deleted = conn.delete(dn) if not deleted: conn.unbind() self.dispatch('ldap', '%s [Error DeleteUser] Could not delete %s\n' % (datetime.now(), dn) ) return "Could not delete the user." conn.unbind() self.dispatch('ldap', '%s [Info DeleteUser] Deleted %s\n' % (datetime.now(), dn) ) return True # the class to log all the dispatches # for now everything gets logged into the same logfile, but # I don't forsee that much traffic plus with timestamps and the class name # in the log should be readable class Log(object): name = "log" ldaplog = config['System']['LOGDIR'] + '/ldap.log' # Gets all the dispatches with 'ldap' and writes them into the ldap.log @event_handler('userlookup', 'ldap') @event_handler('createuser', 'ldap') @event_handler('getuserdata', 'ldap') @event_handler('changeuserdata', 'ldap') @event_handler('passwordresetrequest', 'ldap') @event_handler('changepassword', 'ldap') @event_handler('deleteuser', 'ldap') def event_handler_ldap(self, payload): f = open(self.ldaplog, mode='a', encoding='utf-8') f.write(payload) f.close