Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DRAFT: Implement an LDAP Target #13

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lifecycle/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class User:
fullname: str = ""
email: tuple[str] = field(default_factory=tuple)
groups: tuple[Group] = field(default_factory=tuple)
locked: bool = False

def __post_init__(self):

Expand Down
15 changes: 6 additions & 9 deletions lifecycle/source_ldap3.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,21 +81,18 @@ def fetch_users(self):
if connection.entries:
for ldap_entry in connection.entries:
user_account = ldap_entry.entry_attributes_as_dict
ns_account_lock = user_account["nsAccountLock"]
locked = len(ns_account_lock) > 0 and ns_account_lock[0] == "TRUE"

if (
not locked
and len(user_account["uid"]) > 0
and len(user_account["mail"]) > 0
):

if len(user_account["uid"]) > 0 and len(user_account["mail"]) > 0:
uid = user_account["uid"][0]
ns_account_lock = user_account["nsAccountLock"]
locked = len(ns_account_lock) > 0 and ns_account_lock[0] == "TRUE"
user = User(
uid,
forename=user_account["givenName"][0],
surname=user_account["surName"][0],
email=user_account["mail"],
groups=[],
locked=locked,
)
self.users[uid] = user
else:
Expand Down Expand Up @@ -123,7 +120,7 @@ def fetch_groups(self):
else:
description = ""
group = Group(name, description, ldap_group["mail"])

# member is of the format uid=admin,cn=users,cn=accounts,...
for member in ldap_group["member"]:
components = member.split(",")
uid = components[0].split("=")[1]
Expand Down
96 changes: 96 additions & 0 deletions lifecycle/target_ldap3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""Target for pushing users and groups to LDAP"""

import ldap3

from lifecycle.models import User
from lifecycle.model_diff import ModelDifference
from lifecycle.source_ldap3 import SourceLDAP3


# Inherits from LDAP source because we want to reuse their user and group fetching
class TargetLDAP3(SourceLDAP3):
"""Given an LDAP Server config, will provide an interface to push users and groups to said LDAP Server"""

def reconfigure(self, config: dict):
"""Apply new configuration to object.

The newly supplied config entry will be merged over the existing config.
"""
super().reconfigure(self, config)

extra_default_config = {
"target_operations": [
"add_users",
"remove_users",
"modify_users",
],
"new_user_cn_field": "fullname", # username also a good candidate
"new_user_group_path": "cn=users,cn=accounts"
}
self.config = extra_default_config | self.config

@staticmethod
def _user_to_ldap_changes(user: User) -> dict:
# 'uid' is also a field that could go into ldap changes, but we don't expect that to ever change

# XXX: HOW DO I SET WHICH USERS ARE MEMBERS OF GROUPS?
# Get the Groups that the User is a part of,
# Search for them in LDAP
# And update every Group entry to add/remove/update the list of "member"
# to include the User's dn.
# XXX: Hang on, do users' DN start with "uid=" or "cn="?
lock_status = "TRUE" if user.locked else "FALSE"
return {
"givenName": [(ldap3.MODIFY_REPLACE, [user.forename])],
"surName": [(ldap3.MODIFY_REPLACE, [user.surname])],
"nsAccountLock": [(ldap3.MODIFY_REPLACE, [lock_status])],
"mail": [(ldap3.MODIFY_REPLACE, sorted(user.email))],
}


def _find_user_by_uid(self, connection: ldap3.Connection, uid: str) -> str:
"""Searches for a user by its unique uid and returns its dn"""

# "uid", the key to the users dict, is the only part that's guaranteed to be unique
connection.search(
search_base=self.config["base_dn"],
search_filter=f"(uid={uid})",
search_scope=ldap3.SUBTREE,
attributes=[
"uid",
],
)
assert len(connection.entries) == 1, f"UID {uid} is not unique! Lifecycle can't handle this!"
return connection.entries[0].entry_dn

def add_users(self, users: dict[str, User]):
"""Adds the listed users to the starget"""
# XXX: Not 100% sure what a new user's cn should be. configurable??

def remove_users(self, users: dict[str, User]):
"""Removes the listed users from the target"""

connection = self.connect()
for uid, user in users.items():
user_dn = self._find_user_by_uid(self, connection, uid)
connection.delete(user_dn)

def modify_users(self, users: dict[str, User]):
"""Modifies all listed users in the target"""

connection = self.connect()
for uid, user in users.items():
user_dn = self._find_user_by_uid(self, connection, uid)
changes = _user_to_ldap_changes(user)
connection.modify(user_dn, changes)

def sync_users_changes(self, changes: ModelDifference):
"""Synchronises the difference in the users model with the server"""
operations = self.config["target_operations"]
if "add_users" in operations:
self.add_users(changes.added_users)
if "remove_users" in operations:
self.remove_users(changes.removed_users)
if "modify_users" in operations:
self.modify_users(changes.changed_users)