2023-12-04 11:27:04 -05:00
|
|
|
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):
|
2024-01-05 13:57:32 -05:00
|
|
|
sanitized_body = re.sub(r"[^0-9A-Za-z_ -.]+", ".", name)
|
2023-12-04 11:27:04 -05:00
|
|
|
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):
|
2024-01-05 13:57:32 -05:00
|
|
|
return re.sub(r"[^0-9a-z.]+", ".", name.lower()).strip(".")
|
2023-12-04 11:27:04 -05:00
|
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2024-01-05 14:50:15 -05:00
|
|
|
if member.email:
|
|
|
|
user.props["e-mail"] = [member.email] # ([]) E-mail address
|
|
|
|
|
2023-12-04 11:27:04 -05:00
|
|
|
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())
|