diff --git a/dal/userservice.conf.example b/dal/userservice.conf.example deleted file mode 100644 index 17074da..0000000 --- a/dal/userservice.conf.example +++ /dev/null @@ -1,19 +0,0 @@ -[System] - -# Set up where the RabbitMQ is at and user:password - -RABBITMQ = guest:guest@127.0.0.1 - -[EMAIL] - -EMAILFROM = info@ungleich.ch - -[LDAP] - -# Set up the user who can search -SEARCHUSER = uid=search,ou=system,dc=ungleich,dc=ch -SEARCHUSERPASSWORD = fnord - -# Set up which LDAP server to query for auth - -LDAPSERVER = ldaps://ldap1.ungleich.ch diff --git a/nameko-func.py b/nameko-func.py deleted file mode 100644 index d43a4bc..0000000 --- a/nameko-func.py +++ /dev/null @@ -1,350 +0,0 @@ -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 - diff --git a/nameko.conf.example b/nameko.conf.example deleted file mode 100644 index 53c0b66..0000000 --- a/nameko.conf.example +++ /dev/null @@ -1,20 +0,0 @@ -[System] - -# where the rabbitmq server lives and how to connect -RABBITMQ = guest:guest@localhost - -# where to put the ldap.log -LOGDIR = /home/downhill/ungleich/dal/ - -[LDAP] - -# How many ldapservers are supplied -SERVERMULTIPLE = 1 - -# the list of ldapservers, just number sequentially -LDAPSERVER1 = localhost - -# Change to something which has enough access to create users, change things around, etc -LDAPMANAGER = cn=manager,dc=ungleich,dc=ch -LDAPMANAGERPASSWORD = foobar - diff --git a/requirements-os.txt b/requirements-os.txt new file mode 100644 index 0000000..13b2452 --- /dev/null +++ b/requirements-os.txt @@ -0,0 +1,2 @@ +# Debian/Devuanp +apt install libldap2-dev libsasl2-dev diff --git a/requirements.txt b/requirements.txt index 8178308..e022bb6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,5 @@ django>=2.1.2 django-auth-ldap>=1.7.0 -nameko>=2.11.0 ldap3>=2.5.1 -django-nameko>=0.1 django-bootstrap3>=11.0.0 django-compressor>=2.2 -django-sezikai>=0.10.0