import asyncio import random import re import string from django.conf import settings from udm_rest_client.udm import UDM from udm_rest_client.exceptions import NoObject, UdmError from membershipworks.models import Member, Flag USER_BASE = "cn=users,dc=sawtooth,dc=claremontmakerspace,dc=org" GROUP_BASE = "cn=groups,dc=sawtooth,dc=claremontmakerspace,dc=org" RAND_PW_LEN = 20 GROUPS_REGEX = "|".join( ["Certified: .*", "Access .*\\?", "CMS .*", "Volunteer: .*", "Database .*"] ) # From an API error message: # A group name must start and end with a letter, number or underscore. # In between additionally spaces, dashes and dots are allowed. def sanitize_group_name(name): sanitized_body = re.sub(r"[^0-9A-Za-z_ -.]+", ".", name) sanitized_start_end = re.sub("^[^0-9A-Za-z_]|[^0-9A-Za-z_]$", "_", sanitized_body) return "MW_" + sanitized_start_end # From an API error message: "Username must only contain numbers, letters and dots!" def sanitize_user_name(name): return re.sub(r"[^0-9a-z.]+", ".", name.lower()).strip(".") async def make_groups(udm): group_mod = udm.get("groups/group") existing_groups = {g.props.name async for g in group_mod.search()} flag_groups = { sanitize_group_name(flag.name) async for flag in Flag.objects.filter( type__in=["label", "folder"], name__regex=GROUPS_REGEX ) } missing_groups = flag_groups - existing_groups print(f"Creating missing groups: {missing_groups}") for group_name in missing_groups: group = await group_mod.new() group.props.name = group_name await group.save() async def sync_member(user_mod, member: Member): username = sanitize_user_name(member.account_name) try: # try to get an existing user to update user = await user_mod.get(f"uid={username},{USER_BASE}") except NoObject: # create a new user print("Creating new user {username}") # TODO: search by employeeNumber and rename users when needed user = await user_mod.new() # set a random password and ensure it is changed at next login user.props.password = "".join( random.choice(string.ascii_letters + string.digits) for x in range(0, RAND_PW_LEN) ) user.props.pwdChangeNextLogin = True user.props.update( { "title": "", # Title "firstname": member.first_name, "lastname": member.last_name, # (c) "username": username, # (cmr) "description": "", # Description # "password": "", # (c) Password # "mailPrimaryAddress": member["Email"], # Primary e-mail address # "displayName": "", # Display name # "birthday": "", # Birthdate # "jpegPhoto": "", # Picture of the user (JPEG format) "employeeNumber": member.uid, # "employeeType": "", # Employee type "homedrive": "H:", # Windows home drive "sambahome": f"\\\\ucs\\{username}", # Windows home path "profilepath": "%LOGONSERVER%\\%USERNAME%\\windows-profiles\\default", # Windows profile directory "disabled": not member.is_active, # "userexpiry": member["Renewal Date"], # "pwdChangeNextLogin": "1", # User has to change password on next login # "sambaLogonHours": "", # Permitted times for Windows logins "phone": [member.phone], # Telephone number # "PasswordRecoveryMobile": member["Phone"], # Mobile phone number "PasswordRecoveryEmail": member.email, } ) if member.email: user.props["e-mail"] = [member.email] # ([]) E-mail address new_groups = [ f"cn={sanitize_group_name(flag.name)},{GROUP_BASE}" async for flag in member.flags.filter( type__in=["label", "folder"], name__regex=GROUPS_REGEX ) ] # groups not from this script other_old_groups = [g for g in user.props.groups if not g[3:].startswith("MW_")] user.props.groups = other_old_groups + new_groups try: await user.save() except UdmError: print("Failed to save user", username) print(user.props) raise async def async_accounts(): async with UDM(**settings.UCS) as udm: print("Syncing groups") await make_groups(udm) print("Syncing members") user_mod = udm.get("users/user") async for member in Member.objects.with_is_active().filter( Member.objects.has_flag("folder", "Members") | Member.objects.has_flag("folder", "CMS Staff") ): await sync_member(user_mod, member) def sync_accounts(): asyncio.run(async_accounts())