mirror of https://github.com/wekan/wekan
Thanks to hpvb ! Related #4740, related #4739, related #4738, related #4737, related #4736pull/4751/head
parent
9b82ce149f
commit
ca9d47c2aa
@ -0,0 +1,438 @@ |
|||||||
|
#!/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()) |
||||||
|
|
||||||
|
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() |
Loading…
Reference in new issue