diff options
-rw-r--r-- | arthur/apis/directory/freeipa.py | 7 | ||||
-rw-r--r-- | arthur/apis/directory/ldap.py | 4 | ||||
-rw-r--r-- | arthur/exts/directory/ldap.py | 112 |
3 files changed, 78 insertions, 45 deletions
diff --git a/arthur/apis/directory/freeipa.py b/arthur/apis/directory/freeipa.py index cf4ba23..ef0f1b3 100644 --- a/arthur/apis/directory/freeipa.py +++ b/arthur/apis/directory/freeipa.py @@ -68,6 +68,13 @@ def deactivate_user(username: str) -> None: client.user_mod(username, o_nsaccountlock=True) +def activate_user(username: str) -> None: + """Activate a user in FreeIPA.""" + client = create_client() + + client.user_mod(username, o_nsaccountlock=False) + + def create_user(username: str, display_name: str, groups: list[str], discord_id: int) -> str: """ Create a new user in FreeIPA. If the user exists, the password is reset and returned. diff --git a/arthur/apis/directory/ldap.py b/arthur/apis/directory/ldap.py index c919f55..7af4c08 100644 --- a/arthur/apis/directory/ldap.py +++ b/arthur/apis/directory/ldap.py @@ -22,6 +22,7 @@ class LDAPUser: employee_number: str | None = None display_name: str | None = None groups: list[str] | None = None + locked: bool = False def prepare_dn(dn: str) -> str: @@ -60,7 +61,7 @@ async def find_users() -> list[LDAPUser]: prepare_dn("cn=users,cn=accounts"), LDAPSearchScope.SUBTREE, "(mail=*@pydis.wtf)", - ["uid", "employeeNumber", "displayName", "memberOf"], + ["uid", "employeeNumber", "displayName", "memberOf", "nsAccountLock"], ) for user in users: @@ -77,6 +78,7 @@ async def find_users() -> list[LDAPUser]: employee_number=user.get("employeeNumber", [None])[0], display_name=user["displayName"][0], groups=parsed_groups, + locked=user.get("nsAccountLock", [False])[0], ) found_users.append(new_user) diff --git a/arthur/exts/directory/ldap.py b/arthur/exts/directory/ldap.py index 366a42e..72cb460 100644 --- a/arthur/exts/directory/ldap.py +++ b/arthur/exts/directory/ldap.py @@ -89,6 +89,7 @@ class LDAPSyncAction(StrEnum): REMOVE = "remove" KEEP = "keep" CHANGE = "change" + NO_ACTION = "no_action" @dataclass @@ -154,58 +155,72 @@ class LDAP(commands.Cog): @tasks.loop(minutes=10) async def sync_users(self) -> None: """Sync users with the LDAP directory.""" - logger.info("Syncing users with the LDAP directory.") + try: + logger.info("Syncing users with the LDAP directory.") - diff, missing_emp, counts = await self.get_user_diff() - - add_users = counts[LDAPSyncAction.ADD] - remove_users = counts[LDAPSyncAction.REMOVE] - keep_users = counts[LDAPSyncAction.KEEP] - change_users = counts[LDAPSyncAction.CHANGE] + diff, missing_emp, counts = await self.get_user_diff() - logger.info( - f"LDAP: {add_users} missing users, removing {remove_users} users, " - f"keeping {keep_users} users, and changing {change_users} users." - ) + add_users = counts[LDAPSyncAction.ADD] + remove_users = counts[LDAPSyncAction.REMOVE] + keep_users = counts[LDAPSyncAction.KEEP] + change_users = counts[LDAPSyncAction.CHANGE] + no_action_users = counts[LDAPSyncAction.NO_ACTION] - if len(missing_emp) > 0: - logger.error( - "LDAP: Some users are missing an employee number. This may lead to duplicated users being created." + logger.info( + f"LDAP: {add_users} missing users, removing {remove_users} users, " + f"keeping {keep_users} users, changing {change_users} users and no action for {no_action_users} users." ) - await self.bot.get_channel(CONFIG.devops_channel_id).send( - ":x: LDAP Sync: Some users are missing an employee number. This may lead to duplicate users, please rectify." - ) + if len(missing_emp) > 0: + logger.error( + "LDAP: Some users are missing an employee number. This may lead to duplicated users being created." + ) + + await self.bot.get_channel(CONFIG.devops_channel_id).send( + ":x: LDAP Sync: Some users are missing an employee number. This may lead to duplicate users, please rectify." + ) - notified_users = [] + notified_users = [] - async for message in self.bot.get_channel(CONFIG.ldap_bootstrap_channel_id).history( - limit=None, oldest_first=True - ): - if ( - "Python Discord LDAP enrollment" in message.content - or len(message.mentions) == 0 - or message.author != self.bot.user + async for message in self.bot.get_channel(CONFIG.ldap_bootstrap_channel_id).history( + limit=None, oldest_first=True ): - continue + if ( + "Python Discord LDAP enrollment" in message.content + or len(message.mentions) == 0 + or message.author != self.bot.user + ): + continue - notified_users.append(message.mentions[0]) + notified_users.append(message.mentions[0]) - for user in diff: - if user.action == LDAPSyncAction.ADD: - if user.discord_user in notified_users: - continue + for user in diff: + await self._process_user(user, notified_users) - if NOTIFICATIONS_ENABLED: - await self.bot.get_channel(CONFIG.ldap_bootstrap_channel_id).send( - ELIGIBLE_MESSAGE.format(mention=user.discord_user.mention) - ) - if user.action == LDAPSyncAction.REMOVE: - freeipa.deactivate_user(user.ldap_user.uid) - elif user.action == LDAPSyncAction.CHANGE: - freeipa.set_user_groups(user.ldap_user.uid, user.groups) + logger.info("LDAP: Sync complete.") + except Exception as e: # noqa: BLE001 + logger.exception(f"LDAP: Error during sync: {e}", exc_info=True) + await self.bot.get_channel(CONFIG.devops_channel_id).send( + f":x: LDAP Sync Error: ```python\n{e}```" + ) + + async def _process_user(self, user: DiffedUser, notified_users: list[discord.User]) -> None: + if user.action == LDAPSyncAction.ADD: + if user.discord_user in notified_users: + return - logger.info("LDAP: Sync complete.") + if NOTIFICATIONS_ENABLED: + await self.bot.get_channel(CONFIG.ldap_bootstrap_channel_id).send( + ELIGIBLE_MESSAGE.format(mention=user.discord_user.mention) + ) + if user.action == LDAPSyncAction.KEEP: + if user.ldap_user and user.ldap_user.locked: + freeipa.activate_user(user.ldap_user.uid) + if user.action == LDAPSyncAction.REMOVE: + if user.ldap_user and not user.ldap_user.locked: + freeipa.deactivate_user(user.ldap_user.uid) + elif user.action == LDAPSyncAction.CHANGE: + freeipa.set_user_groups(user.ldap_user.uid, user.groups) async def cleanup_bootstrap(self, user: discord.Member) -> None: """Clear up the bootstrap message for a user.""" @@ -346,9 +361,12 @@ class LDAP(commands.Cog): if base_role not in user.roles: if user.id in ldap_discord_id_map: - diff.append( - DiffedUser(user, ldap_discord_id_map[user.id], [], LDAPSyncAction.REMOVE) + action = ( + LDAPSyncAction.NO_ACTION + if ldap_discord_id_map[user.id].locked + else LDAPSyncAction.REMOVE ) + diff.append(DiffedUser(user, ldap_discord_id_map[user.id], [], action)) continue user_role_ids = {r.id for r in user.roles} @@ -364,9 +382,12 @@ class LDAP(commands.Cog): else: diff.append(DiffedUser(user, None, roles, LDAPSyncAction.ADD)) elif user.id in ldap_discord_id_map: - diff.append( - DiffedUser(user, ldap_discord_id_map[user.id], [], LDAPSyncAction.REMOVE) + action = ( + LDAPSyncAction.NO_ACTION + if ldap_discord_id_map[user.id].locked + else LDAPSyncAction.REMOVE ) + diff.append(DiffedUser(user, ldap_discord_id_map[user.id], [], action)) counter = Counter([user.action for user in diff]) @@ -389,6 +410,7 @@ class LDAP(commands.Cog): remove_users = counts[LDAPSyncAction.REMOVE] keep_users = counts[LDAPSyncAction.KEEP] change_users = counts[LDAPSyncAction.CHANGE] + no_action_users = counts[LDAPSyncAction.NO_ACTION] diff_message = "# LDAP Sync Overview\n" @@ -396,6 +418,7 @@ class LDAP(commands.Cog): diff_message += f"**Removing Users:** {remove_users}\n" diff_message += f"**Keeping Users:** {keep_users}\n" diff_message += f"**Changing Users:** {change_users}\n" + diff_message += f"**No Action Required:** {no_action_users}\n\n" diff_message += "```diff\n" @@ -406,6 +429,7 @@ class LDAP(commands.Cog): LDAPSyncAction.REMOVE: "-", LDAPSyncAction.KEEP: " ", LDAPSyncAction.CHANGE: "~", + LDAPSyncAction.NO_ACTION: "#", } messages = [diff_message] |