3 changed files with 457 additions and 60 deletions
@ -0,0 +1,350 @@
|
||||
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 |
||||
|
Loading…
Reference in new issue