pyceo/ceod/model/LDAPService.py

219 lines
7.6 KiB
Python
Raw Normal View History

2021-07-19 01:47:39 -04:00
import copy
import grp
import pwd
2021-08-03 10:09:07 -04:00
from typing import Union, List
2021-07-19 01:47:39 -04:00
import ldap
import ldap.modlist
from zope import component
from zope.interface import implementer
2021-08-03 19:19:33 -04:00
from ceo_common.errors import UserNotFoundError, GroupNotFoundError, \
UserAlreadyExistsError, GroupAlreadyExistsError
2021-08-03 10:09:07 -04:00
from ceo_common.interfaces import ILDAPService, IKerberosService, IConfig, \
IUser, IGroup, IUWLDAPService
2021-07-19 01:47:39 -04:00
from .User import User
from .Group import Group
2021-07-23 20:08:22 -04:00
from .SudoRole import SudoRole
2021-08-03 10:09:07 -04:00
from .utils import dn_to_uid, bytes_to_strings
2021-07-19 01:47:39 -04:00
@implementer(ILDAPService)
class LDAPService:
def __init__(self):
cfg = component.getUtility(IConfig)
self.ldap_admin_principal = cfg.get('ldap_admin_principal')
self.ldap_server_url = cfg.get('ldap_server_url')
self.ldap_users_base = cfg.get('ldap_users_base')
self.ldap_groups_base = cfg.get('ldap_groups_base')
2021-08-03 19:19:33 -04:00
self.member_min_id = cfg.get('members_min_id')
self.member_max_id = cfg.get('members_max_id')
self.club_min_id = cfg.get('clubs_min_id')
self.club_max_id = cfg.get('clubs_max_id')
2021-07-19 01:47:39 -04:00
def _get_ldap_conn(self, gssapi_bind: bool = True) -> ldap.ldapobject.LDAPObject:
2021-07-23 20:08:22 -04:00
# TODO: cache the connection
2021-07-19 01:47:39 -04:00
conn = ldap.initialize(self.ldap_server_url)
if gssapi_bind:
self._gssapi_bind(conn)
return conn
def _gssapi_bind(self, conn: ldap.ldapobject.LDAPObject):
krb_srv = component.getUtility(IKerberosService)
for i in range(2):
try:
conn.sasl_gssapi_bind_s()
return
except ldap.LOCAL_ERROR as err:
if 'Ticket expired' in err.args[0]['info']:
krb_srv.kinit()
continue
raise err
raise Exception('could not perform GSSAPI bind')
def get_user(self, username: str) -> IUser:
conn = self._get_ldap_conn(False)
2021-08-03 10:09:07 -04:00
base = self.uid_to_dn(username)
2021-07-19 01:47:39 -04:00
try:
_, result = conn.search_s(base, ldap.SCOPE_BASE)[0]
2021-07-24 17:09:10 -04:00
return User.deserialize_from_ldap(result)
2021-07-19 01:47:39 -04:00
except ldap.NO_SUCH_OBJECT:
raise UserNotFoundError()
def get_group(self, cn: str) -> IGroup:
conn = self._get_ldap_conn(False)
2021-08-03 10:09:07 -04:00
base = self.group_cn_to_dn(cn)
2021-07-19 01:47:39 -04:00
try:
_, result = conn.search_s(base, ldap.SCOPE_BASE)[0]
2021-07-24 17:09:10 -04:00
return Group.deserialize_from_ldap(result)
2021-07-19 01:47:39 -04:00
except ldap.NO_SUCH_OBJECT:
raise GroupNotFoundError()
2021-08-03 10:09:07 -04:00
def uid_to_dn(self, uid: str):
return f'uid={uid},{self.ldap_users_base}'
def group_cn_to_dn(self, cn: str):
return f'cn={cn},{self.ldap_groups_base}'
2021-07-19 01:47:39 -04:00
def _get_next_uid(self, conn: ldap.ldapobject.LDAPObject, min_id: int, max_id: int) -> int:
"""Gets the next available UID number between min_id and max_id, inclusive."""
def uid_exists(uid: int) -> bool:
try:
pwd.getpwuid(uid)
return True
except KeyError:
return False
def gid_exists(gid: int) -> bool:
try:
grp.getgrgid(gid)
return True
except KeyError:
return False
def ldap_uid_or_gid_exists(uid: int) -> bool:
results = conn.search_s(
self.ldap_users_base, ldap.SCOPE_ONELEVEL,
f'(|(uidNumber={uid})(gidNumber={uid}))')
return len(results) > 0
# TODO: replace this with binary search
for uid in range(min_id, max_id + 1):
if uid_exists(uid) or gid_exists(uid) or ldap_uid_or_gid_exists(uid):
continue
return uid
raise Exception('no UIDs remaining')
2021-07-23 20:08:22 -04:00
def add_sudo_role(self, uid: str):
conn = self._get_ldap_conn()
sudo_role = SudoRole(uid)
2021-07-24 17:09:10 -04:00
modlist = ldap.modlist.addModlist(sudo_role.serialize_for_ldap())
2021-07-23 20:08:22 -04:00
conn.add_s(sudo_role.dn, modlist)
2021-07-24 17:09:10 -04:00
def remove_sudo_role(self, uid: str):
conn = self._get_ldap_conn()
sudo_role = SudoRole(uid)
conn.delete_s(sudo_role.dn)
2021-07-23 20:08:22 -04:00
def add_user(self, user: IUser) -> IUser:
2021-07-19 01:47:39 -04:00
if user.is_club():
min_id, max_id = self.club_min_id, self.club_max_id
else:
min_id, max_id = self.member_min_id, self.member_max_id
conn = self._get_ldap_conn()
uid_number = self._get_next_uid(conn, min_id, max_id)
2021-07-24 17:09:10 -04:00
new_user = copy.copy(user)
2021-07-19 01:47:39 -04:00
new_user.uid_number = uid_number
new_user.gid_number = uid_number
2021-07-24 17:09:10 -04:00
modlist = ldap.modlist.addModlist(new_user.serialize_for_ldap())
2021-08-03 19:19:33 -04:00
try:
conn.add_s(new_user.dn, modlist)
except ldap.ALREADY_EXISTS:
raise UserAlreadyExistsError()
2021-07-19 01:47:39 -04:00
return new_user
2021-07-24 17:09:10 -04:00
def modify_user(self, old_user: IUser, new_user: IUser):
conn = self._get_ldap_conn()
modlist = ldap.modlist.modifyModlist(
old_user.serialize_for_ldap(),
new_user.serialize_for_ldap(),
)
conn.modify_s(old_user.dn, modlist)
def remove_user(self, user: IUser):
conn = self._get_ldap_conn()
conn.delete_s(user.dn)
2021-07-23 20:08:22 -04:00
def add_group(self, group: IGroup) -> IGroup:
2021-07-19 01:47:39 -04:00
conn = self._get_ldap_conn()
# make sure that the caller initialized the GID number
assert group.gid_number
2021-07-24 17:09:10 -04:00
modlist = ldap.modlist.addModlist(group.serialize_for_ldap())
2021-08-03 19:19:33 -04:00
try:
conn.add_s(group.dn, modlist)
except ldap.ALREADY_EXISTS:
raise GroupAlreadyExistsError()
2021-07-19 01:47:39 -04:00
return group
def modify_group(self, old_group: IGroup, new_group: IGroup):
conn = self._get_ldap_conn()
modlist = ldap.modlist.modifyModlist(
2021-07-24 17:09:10 -04:00
old_group.serialize_for_ldap(),
new_group.serialize_for_ldap(),
2021-07-19 01:47:39 -04:00
)
conn.modify_s(old_group.dn, modlist)
2021-07-24 17:09:10 -04:00
def remove_group(self, group: IGroup):
conn = self._get_ldap_conn()
conn.delete_s(group.dn)
2021-08-03 10:09:07 -04:00
def update_programs(
self,
dry_run: bool = False,
members: Union[List[str], None] = None,
uwldap_batch_size: int = 100,
):
if members:
filter_str = '(|' + ''.join([
f'(uid={uid})' for uid in members
]) + ')'
else:
filter_str = None
conn = self._get_ldap_conn()
raw_csc_records = conn.search_s(
self.ldap_users_base, ldap.SCOPE_SUBTREE, filter_str,
attrlist=['program'])
uids = [
dn_to_uid(dn) for dn, _ in raw_csc_records
]
csc_programs = [
bytes_to_strings(data).get('program', [None])[0]
for _, data in raw_csc_records
]
uwldap_srv = component.getUtility(IUWLDAPService)
uw_programs = []
# send queries in small batches
for i in range(0, len(csc_programs), uwldap_batch_size):
batch_uids = uids[i:i + uwldap_batch_size]
batch_uw_programs = uwldap_srv.get_programs_for_users(batch_uids)
uw_programs.extend(batch_uw_programs)
users_to_change = [
(uids[i], csc_programs[i], uw_programs[i])
for i in range(len(uids))
if csc_programs[i] != uw_programs[i] and (
uw_programs[i] not in (None, 'expired', 'orphaned')
)
]
if dry_run:
return users_to_change
for uid, old_program, new_program in users_to_change:
old_entry = {'program': [old_program.encode()]}
new_entry = {'program': [new_program.encode()]}
modlist = ldap.modlist.modifyModlist(old_entry, new_entry)
conn.modify_s(self.uid_to_dn(uid), modlist)
return users_to_change