| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461 | #!/usr/bin/env python3# ChangeLog# ---------# 2022-10-29:#   LDAP sync script added, thanks to hpvb:#   - syncs LDAP teams and avatars to WeKan MongoDB database#   - removes or disables WeKan users that are also disabled at LDAP#   TODO:#   - There is hardcoded value of avatar URL example.com .#     Try to change it to use existing environment variables.import osimport environsimport ldapimport hashlibfrom pymongo import MongoClientfrom pymongo.errors import DuplicateKeyErrorenv = environs.Env()stats = {  'created': 0,  'updated': 0,  'disabled': 0,  'team_created': 0,  'team_updated': 0,  'team_disabled': 0,  'team_membership_update': 0,  'board_membership_update': 0}mongodb_client = MongoClient(env('MONGO_URL'))mongodb_database = mongodb_client[env('MONGO_DBNAME')]class LdapConnection:    def __init__(self):        self.url = env('LDAP_URL')        self.binddn = env('LDAP_BINDDN', default='')        self.bindpassword = env('LDAP_BINDPASSWORD', default='')        self.basedn = env('LDAP_BASEDN')        self.group_base = env('LDAP_GROUP_BASE')        self.group_name_attribute = env('LDAP_GROUP_NAME_ATTRIBUTE')        self.admin_group = env('LDAP_ADMIN_GROUP', default=None)        self.user_base = env('LDAP_USER_BASE')        self.user_group = env('LDAP_USER_GROUP', default=None)        self.user_objectclass = env('LDAP_USER_OBJECTCLASS')        self.user_username_attribute = env('LDAP_USER_USERNAME_ATTRIBUTE')        self.user_fullname_attribute = env('LDAP_USER_FULLNAME_ATTRIBUTE')        self.user_email_attribute = env('LDAP_USER_EMAIL_ATTRIBUTE')        self.user_photo_attribute = env('LDAP_USER_PHOTO_ATTRIBUTE', default=None)        self.user_attributes = [ "memberOf", "entryUUID", "initials", self.user_username_attribute, self.user_fullname_attribute, self.user_email_attribute ]        if self.user_photo_attribute:             self.user_attributes.append(self.user_photo_attribute)        self.con = ldap.initialize(self.url)        self.con.simple_bind_s(self.binddn, self.bindpassword)    def get_groups(self):        search_base = f"{self.group_base},{self.basedn}"        search_filter=f"(objectClass=groupOfNames)"        res = self.con.search(search_base, ldap.SCOPE_SUBTREE, search_filter, ['cn', 'description', 'o', 'entryUUID'])        result_set = {}        while True:            result_type, result_data = self.con.result(res, 0)            if (result_data == []):                break            else:                if result_type == ldap.RES_SEARCH_ENTRY:                    ldap_data = {}                    data = {}                    for attribute in result_data[0][1]:                        ldap_data[attribute] = [ val.decode() for val in result_data[0][1][attribute] ]                    try:                        data['dn'] = result_data[0][0]                        data['name'] = ldap_data['cn'][0]                        data['uuid'] = ldap_data['entryUUID'][0]                        try:                            data['description'] = ldap_data['description'][0]                        except KeyError:                            data['description'] = data['name']                        result_set[data['name']] = data                    except KeyError as e:                        print(f"Skipping Ldap object {result_data[0][0]}, missing attribute {e}.")        return result_set    def get_group_name(self, dn):        res = self.con.search(dn, ldap.SCOPE_BASE, None, [self.group_name_attribute])        result_type, result_data = self.con.result(res, 0)        if result_type == ldap.RES_SEARCH_ENTRY:            return result_data[0][1][self.group_name_attribute][0].decode()    def get_users(self):        search_base = f"{self.user_base},{self.basedn}"        search_filter = ""                if self.user_group:            search_filter=f"(&(objectClass={self.user_objectclass})(memberof={self.user_group},{self.basedn}))"        else:            search_filter=f"(objectClass={self.user_objectclass})"                  ldap_groups = self.get_groups()        res = self.con.search(search_base, ldap.SCOPE_SUBTREE, search_filter, self.user_attributes)        result_set = {}        while True:            result_type, result_data = self.con.result(res, 0)            if (result_data == []):                break            else:                if result_type == ldap.RES_SEARCH_ENTRY:                    ldap_data = {}                    data = {}                    for attribute in result_data[0][1]:                        if attribute == self.user_photo_attribute:                            ldap_data[attribute] = result_data[0][1][attribute]                        else:                            ldap_data[attribute] = [ val.decode() for val in result_data[0][1][attribute] ]                    try:                        data['dn'] = result_data[0][0]                        data['username'] = ldap_data[self.user_username_attribute][0]                        data['full_name'] = ldap_data[self.user_fullname_attribute][0]                        data['email'] = ldap_data[self.user_email_attribute][0]                        data['uuid'] = ldap_data['entryUUID'][0]                        try:                            data['initials'] = ldap_data['initials'][0]                        except KeyError:                            data['initials'] = ''                        try:                            data['photo'] = ldap_data[self.user_photo_attribute][0]                            data['photo_hash'] = hashlib.md5(data['photo']).digest()                        except KeyError:                            data['photo'] = None                        data['is_superuser'] = f"{self.admin_group},{self.basedn}" in ldap_data['memberOf']                        data['groups'] = []                        for group in ldap_data['memberOf']:                            if group.endswith(f"{self.group_base},{self.basedn}"):                                data['groups'].append(ldap_groups[self.get_group_name(group)])                        result_set[data['username']] = data                    except KeyError as e:                        print(f"Skipping Ldap object {result_data[0][0]}, missing attribute {e}.")        return result_setdef create_wekan_user(ldap_user):    user = { "_id": ldap_user['uuid'],            "username": ldap_user['username'],            "emails": [ { "address": ldap_user['email'], "verified": True } ],            "isAdmin": ldap_user['is_superuser'],            "loginDisabled": False,            "authenticationMethod": 'oauth2',            "sessionData": {},            "importUsernames": [ None ],            "teams": [],            "orgs": [],            "profile": {                "fullname": ldap_user['full_name'],                "avatarUrl": f"https://example.com/user/profile_picture/{ldap_user['username']}",                "initials": ldap_user['initials'],                "boardView": "board-view-swimlanes",                "listSortBy": "-modifiedAt",            },            "services": {                "oidc": {                    "id": ldap_user['username'],                    "username": ldap_user['username'],                    "fullname": ldap_user['full_name'],                    "email": ldap_user['email'],                    "groups": [],                },            },    }    try:        mongodb_database["users"].insert_one(user)        print(f"Creating new Wekan user {ldap_user['username']}")        stats['created'] += 1    except DuplicateKeyError:        print(f"Wekan user {ldap_user['username']} already exists.")        update_wekan_user(ldap_user)def update_wekan_user(ldap_user):    updated = False    user = mongodb_database["users"].find_one({"username": ldap_user['username']})    if user["emails"][0]["address"] != ldap_user['email']:        updated = True        user["emails"][0]["address"] = ldap_user['email']    if user["emails"][0]["verified"] != True:        updated = True        user["emails"][0]["verified"] = True    if user["isAdmin"] != ldap_user['is_superuser']:        updated = True        user["isAdmin"] = ldap_user['is_superuser']    try:        if user["loginDisabled"] != False:            updated = True            user["loginDisabled"] = False    except KeyError:        updated = True        user["loginDisabled"] = False    if user["profile"]["fullname"] != ldap_user['full_name']:        updated = True        user["profile"]["fullname"] = ldap_user['full_name']    if user["profile"]["avatarUrl"] != f"https://example.com/user/profile_picture/{ldap_user['username']}":        updated = True        user["profile"]["avatarUrl"] = f"https://example.com/user/profile_picture/{ldap_user['username']}"    if user["profile"]["initials"] != ldap_user['initials']:        updated = True        user["profile"]["initials"] = ldap_user['initials']    if user["services"]["oidc"]["fullname"] != ldap_user['full_name']:        updated = True        user["services"]["oidc"]["fullname"] = ldap_user['full_name']    if user["services"]["oidc"]["email"] != ldap_user['email']:        updated = True        user["services"]["oidc"]["email"] = ldap_user['email']    if updated:        print(f"Updated Wekan user {ldap_user['username']}")        stats['updated'] += 1        mongodb_database["users"].update_one({"username": ldap_user['username']}, {"$set": user})def disable_wekan_user(username):    print(f"Disabling Wekan user {username}")    stats['disabled'] += 1    mongodb_database["users"].update_one({"username": username}, {"$set": {"loginDisabled": True}})def create_wekan_team(ldap_group):    print(f"Creating new Wekan team {ldap_group['name']}")    stats['team_created'] += 1        team = { "_id": ldap_group['uuid'],            "teamShortName": ldap_group["name"],            "teamDisplayName": ldap_group["name"],            "teamDesc": ldap_group["description"],            "teamWebsite": "http://localhost",            "teamIsActive": True    }    mongodb_database["team"].insert_one(team)def update_wekan_team(ldap_group):    updated = False    team = mongodb_database["team"].find_one({"_id": ldap_group['uuid']})    team_tmp = { "_id": ldap_group['uuid'],            "teamShortName": ldap_group["name"],            "teamDisplayName": ldap_group["name"],            "teamDesc": ldap_group["description"],            "teamWebsite": "http://localhost",            "teamIsActive": True    }    for key, value in team_tmp.items():        try:            if team[key] != value:                updated = True                break        except KeyError:            updated = True    if updated:        print(f"Updated Wekan team {ldap_group['name']}")        stats['team_updated'] += 1        mongodb_database["team"].update_one({"_id": ldap_group['uuid']}, {"$set": team_tmp})def disable_wekan_team(teamname):    print(f"Disabling Wekan team {teamname}")    stats['team_disabled'] += 1    mongodb_database["team"].update_one({"teamShortName": teamname}, {"$set": {"teamIsActive": False}})def update_wekan_team_memberships(ldap_user):    updated = False    user = mongodb_database["users"].find_one({"username": ldap_user['username']})    teams = user["teams"]    teams_tmp = []    for group in ldap_user["groups"]:        teams_tmp.append({            'teamId': group['uuid'],            'teamDisplayName': group['name'],    })    for team in teams_tmp:        if team not in teams:            updated = True            break    if len(teams) != len(teams_tmp):        updated = True    if updated:        print(f"Updated Wekan team memberships for {ldap_user['username']}")        stats['team_membership_update'] += 1        mongodb_database["users"].update_one({"username": ldap_user['username']}, {"$set": { "teams" : teams_tmp }})def update_wekan_board_memberships(ldap_users):    for board in mongodb_database["boards"].find():        try:            if board['type'] != 'board':                continue        except KeyError:            continue        if not "teams" in board.keys():            continue        members = []        if "members" in board.keys():            members = board["members"]        members_tmp = []        for team in board["teams"]:            for username, user in ldap_users.items():                for group in user["groups"]:                    if group['uuid'] == team['teamId']:                        user_tmp = {                            'userId': user['uuid'],                            'isAdmin': user['is_superuser'],                            'isActive': True,                            'isNoComments': False,                            'isCommentOnly': False,                            'isWorker': False                        }                        if user_tmp not in members_tmp:                            members_tmp.append(user_tmp.copy())        board_users = []        for card in mongodb_database["cards"].find({"boardId": board['_id']}):            if card['userId'] not in board_users:                board_users.append(card['userId'])        inactive_board_users = board_users.copy()        for member in members_tmp:            if member['userId'] in board_users:                inactive_board_users.remove(member['userId'])        for inactive_board_user in inactive_board_users:            user_tmp = {                'userId': inactive_board_user,                'isAdmin': False,                'isActive': False,                'isNoComments': False,                'isCommentOnly': False,                'isWorker': False            }            if user_tmp not in members_tmp:                members_tmp.append(user_tmp.copy())        if members != members_tmp:            print(f"Updated Wekan board membership for {board['title']}")            stats['board_membership_update'] += 1            mongodb_database["boards"].update_one({"_id": board["_id"]}, {"$set": { "members" : members_tmp }})    def ldap_sync():    print("Fetching users from LDAP")    ldap = LdapConnection()    ldap_users = ldap.get_users()    ldap_username_list = ldap_users.keys()    print("Fetching users from Wekan")    wekan_username_list = []    for user in mongodb_database["users"].find():        if not user['loginDisabled']:            wekan_username_list.append(user['username'])    print("Sorting users")    not_in_ldap = []    not_in_wekan = []    in_wekan = []    for ldap_username in ldap_username_list:        if ldap_username in wekan_username_list:           in_wekan.append(ldap_username)        else:           not_in_wekan.append(ldap_username)    for wekan_username in wekan_username_list:        if wekan_username not in ldap_username_list:            not_in_ldap.append(wekan_username)    print("Fetching groups from LDAP")    ldap_groups = ldap.get_groups()    ldap_groupname_list = ldap_groups.keys()    print("Fetching teams from Wekan")    wekan_teamname_list = []    for team in mongodb_database["team"].find():        if team['teamIsActive']:            wekan_teamname_list.append(team['teamShortName'])    print("Sorting groups")    group_not_in_ldap = []    group_not_in_wekan = []    group_in_wekan = []    for ldap_groupname in ldap_groupname_list:        if ldap_groupname in wekan_teamname_list:           group_in_wekan.append(ldap_groupname)        else:           group_not_in_wekan.append(ldap_groupname)    for wekan_teamname in wekan_teamname_list:        if wekan_teamname not in ldap_groupname_list:            group_not_in_ldap.append(wekan_teamname)    print("Processing users")    for user in not_in_wekan:        create_wekan_user(ldap_users[user])    for user in in_wekan:        update_wekan_user(ldap_users[user])    for user in not_in_ldap:        disable_wekan_user(user)    print("Processing groups")    for group in group_not_in_wekan:        create_wekan_team(ldap_groups[group])    for group in group_in_wekan:        update_wekan_team(ldap_groups[group])    for team in group_not_in_ldap:        disable_wekan_team(team)    for username, user in ldap_users.items():        update_wekan_team_memberships(user)    print("Updating board memberships")    update_wekan_board_memberships(ldap_users)    print()    print(f"Total users considered: {len(ldap_username_list)}")    print(f"Total groups considered: {len(ldap_groups)}")    print(f"Users created {stats['created']}")    print(f"Users updated {stats['updated']}")    print(f"Users disabled {stats['disabled']}")    print(f"Teams created {stats['team_created']}")    print(f"Teams updated {stats['team_updated']}")    print(f"Teams disabled {stats['team_disabled']}")    print(f"Team memberships updated: {stats['team_membership_update']}")    print(f"Board memberships updated: {stats['board_membership_update']}")if __name__ == "__main__":    ldap_sync()
 |