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 os
- import environs
- import ldap
- import hashlib
- from pymongo import MongoClient
- from pymongo.errors import DuplicateKeyError
- env = 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_set
- def 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()
|