diff --git a/.drone/adldap_data.ldif b/.drone/adldap_data.ldif new file mode 100644 index 0000000..027eea1 --- /dev/null +++ b/.drone/adldap_data.ldif @@ -0,0 +1,17 @@ +dn: ou=ADLDAP,dc=csclub,dc=internal +objectClass: organizationalUnit +ou: ADLDAP + +dn: cn=alumni1,ou=ADLDAP,dc=csclub,dc=internal +description: One, Alumni +givenName: Alumni +sn: *One +cn: alumni1 +sAMAccountName: alumni1 +displayName: alumni1 +mail: alumni1@alumni.uwaterloo.internal +objectClass: mockADUser +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top diff --git a/.drone/auth1-setup.sh b/.drone/auth1-setup.sh index cf7c45b..2bc9332 100755 --- a/.drone/auth1-setup.sh +++ b/.drone/auth1-setup.sh @@ -31,13 +31,13 @@ IMAGE__setup_ldap() { cp .drone/slapd.conf /etc/ldap/slapd.conf cp .drone/ldap.conf /etc/ldap/ldap.conf cp /usr/share/doc/sudo-ldap/schema.OpenLDAP /etc/ldap/schema/sudo.schema - cp .drone/rfc2307bis.schema /etc/ldap/schema/ - cp .drone/csc.schema /etc/ldap/schema/ + cp .drone/{rfc2307bis,csc,mock_ad}.schema /etc/ldap/schema/ chown -R openldap:openldap /etc/ldap/schema/ /var/lib/ldap/ /etc/ldap/ sleep 0.5 && service slapd start ldapadd -c -f .drone/data.ldif -Y EXTERNAL -H ldapi:/// if [ -z "$CI" ]; then ldapadd -c -f .drone/uwldap_data.ldif -Y EXTERNAL -H ldapi:/// || true + ldapadd -c -f .drone/adldap_data.ldif -Y EXTERNAL -H ldapi:/// || true # setup ldapvi for convenience apt install -y --no-install-recommends vim ldapvi grep -q 'export EDITOR' /root/.bashrc || \ diff --git a/.drone/data.ldif b/.drone/data.ldif index 0cb6012..09096c0 100644 --- a/.drone/data.ldif +++ b/.drone/data.ldif @@ -186,3 +186,28 @@ objectClass: group objectClass: posixGroup cn: office1 gidNumber: 20004 + +dn: uid=alumni1,ou=People,dc=csclub,dc=internal +cn: Alumni One +givenName: Alumni +sn: One +userPassword: {SASL}alumni1@CSCLUB.INTERNAL +loginShell: /bin/bash +homeDirectory: /users/alumni1 +uid: alumni1 +uidNumber: 20005 +gidNumber: 20005 +objectClass: top +objectClass: account +objectClass: posixAccount +objectClass: shadowAccount +objectClass: member +program: Alumni +term: w2024 + +dn: cn=alumni1,ou=Group,dc=csclub,dc=internal +objectClass: top +objectClass: group +objectClass: posixGroup +cn: alumni1 +gidNumber: 20005 diff --git a/.drone/mock_ad.schema b/.drone/mock_ad.schema new file mode 100644 index 0000000..be344a7 --- /dev/null +++ b/.drone/mock_ad.schema @@ -0,0 +1,10 @@ +# Mock Active Directory Schema + +attributetype ( 1.3.6.1.4.1.70000.1.1.1 NAME 'sAMAccountName' + EQUALITY caseIgnoreMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} ) + +objectclass ( 1.3.6.1.4.1.70000.1.2.1 NAME 'mockADUser' + SUP top AUXILIARY + MUST ( sAMAccountName ) ) diff --git a/.drone/slapd.conf b/.drone/slapd.conf index c324588..2631c2d 100644 --- a/.drone/slapd.conf +++ b/.drone/slapd.conf @@ -7,6 +7,7 @@ include /etc/ldap/schema/rfc2307bis.schema include /etc/ldap/schema/inetorgperson.schema include /etc/ldap/schema/sudo.schema include /etc/ldap/schema/csc.schema +include /etc/ldap/schema/mock_ad.schema include /etc/ldap/schema/misc.schema pidfile /var/run/slapd/slapd.pid @@ -40,6 +41,11 @@ access to * by dn="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" manage by * break +# hide most attributes for alumni in mock UWLDAP +access to attrs=cn,sn,givenName,displayName,ou,mail + dn.regex="^uid=alumni[^,]+,ou=(Test)?UWLDAP,dc=csclub,dc=internal$" + by * none + # systems committee get full access access to * by dn="cn=ceod,dc=csclub,dc=internal" write diff --git a/.drone/uwldap_data.ldif b/.drone/uwldap_data.ldif index b1045ac..636f284 100644 --- a/.drone/uwldap_data.ldif +++ b/.drone/uwldap_data.ldif @@ -106,3 +106,18 @@ objectClass: person objectClass: top uid: exec3 mail: exec3@uwaterloo.internal + +dn: uid=alumni1,ou=UWLDAP,dc=csclub,dc=internal +displayName: Alumni One +givenName: Alumni +sn: One +cn: Alumni One +ou: MAT/Mathematics Computer Science +mailLocalAddress: alumni1@uwaterloo.internal +objectClass: inetLocalMailRecipient +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +uid: alumni1 +mail: alumni1@uwaterloo.internal diff --git a/ceo/__main__.py b/ceo/__main__.py index b4dbb57..653cfee 100644 --- a/ceo/__main__.py +++ b/ceo/__main__.py @@ -1,5 +1,4 @@ import os -import socket import sys from zope import component @@ -9,6 +8,7 @@ from .krb_check import krb_check from .tui.start import main as tui_main from ceo_common.interfaces import IConfig, IHTTPClient from ceo_common.model import Config, HTTPClient +from ceo_common.utils import is_in_development def register_services(): @@ -19,8 +19,7 @@ def register_services(): if 'CEO_CONFIG' in os.environ: config_file = os.environ['CEO_CONFIG'] else: - # This is a hack to determine if we're in the dev env or not - if socket.getfqdn().endswith('.csclub.internal'): + if is_in_development(): config_file = './tests/ceo_dev.ini' else: config_file = '/etc/csc/ceo.ini' diff --git a/ceo/cli/utils.py b/ceo/cli/utils.py index 7e31884..8b4c520 100644 --- a/ceo/cli/utils.py +++ b/ceo/cli/utils.py @@ -1,10 +1,9 @@ -import socket - from typing import List, Tuple, Dict import click import requests +from ceo_common.utils import is_in_development from ..utils import space_colon_kv, generic_handle_stream_response from .CLIStreamResponseHandler import CLIStreamResponseHandler @@ -53,6 +52,6 @@ def handle_sync_response(resp: requests.Response): def check_if_in_development() -> bool: """Aborts if we are not currently in the dev environment.""" - if not socket.getfqdn().endswith('.csclub.internal'): + if not is_in_development(): click.echo('This command may only be called during development.') raise Abort() diff --git a/ceo_common/interfaces/IADLDAPService.py b/ceo_common/interfaces/IADLDAPService.py new file mode 100644 index 0000000..66c718c --- /dev/null +++ b/ceo_common/interfaces/IADLDAPService.py @@ -0,0 +1,18 @@ +import typing +from typing import Optional + +from zope.interface import Interface + +if typing.TYPE_CHECKING: + # FIXME: circular import caused by lifting in __init__.py + from ..model.ADLDAPRecord import ADLDAPRecord + + +class IADLDAPService(Interface): + """Represents the AD LDAP database.""" + + def get_user(username: str) -> Optional['ADLDAPRecord']: + """ + Return the LDAP record for the given user, or + None if no such record exists. + """ diff --git a/ceo_common/interfaces/IUWLDAPService.py b/ceo_common/interfaces/IUWLDAPService.py index 01c913e..70906b2 100644 --- a/ceo_common/interfaces/IUWLDAPService.py +++ b/ceo_common/interfaces/IUWLDAPService.py @@ -1,20 +1,23 @@ -from typing import List, Union +import typing +from typing import List, Optional from zope.interface import Interface +if typing.TYPE_CHECKING: + # FIXME: circular import caused by lifting in __init__.py + from ..model.UWLDAPRecord import UWLDAPRecord + class IUWLDAPService(Interface): """Represents the UW LDAP database.""" - def get_user(username: str): + def get_user(username: str) -> Optional['UWLDAPRecord']: """ Return the LDAP record for the given user, or None if no such record exists. - - :rtype: Union[UWLDAPRecord, None] """ - def get_programs_for_users(usernames: List[str]) -> List[Union[str, None]]: + def get_programs_for_users(usernames: List[str]) -> List[Optional[str]]: """ Return the programs for the given users from UWLDAP. If no record or program is found for a user, their entry in diff --git a/ceo_common/interfaces/__init__.py b/ceo_common/interfaces/__init__.py index b819459..a76a3ac 100644 --- a/ceo_common/interfaces/__init__.py +++ b/ceo_common/interfaces/__init__.py @@ -1,3 +1,4 @@ +from .IADLDAPService import IADLDAPService from .ICloudResourceManager import ICloudResourceManager from .ICloudStackService import ICloudStackService from .IKerberosService import IKerberosService diff --git a/ceo_common/model/ADLDAPRecord.py b/ceo_common/model/ADLDAPRecord.py new file mode 100644 index 0000000..fc8cb5d --- /dev/null +++ b/ceo_common/model/ADLDAPRecord.py @@ -0,0 +1,57 @@ +import ldap3 + +from .UWLDAPRecord import UWLDAPRecord + + +class ADLDAPRecord: + """Represents a record from the AD LDAP.""" + + # These are just the ones in which we're interested + ldap_attributes = [ + 'sAMAccountName', + 'mail', + 'description', + 'sn', + 'givenName', + ] + + def __init__( + self, + sam_account_name: str, + mail: str, + description: str, + sn: str, + given_name: str, + ): + self.sam_account_name = sam_account_name + self.mail = mail + self.description = description + self.sn = sn + self.given_name = given_name + + @staticmethod + def deserialize_from_ldap(entry: ldap3.Entry): + """ + Deserializes a dict returned from LDAP into an + ADLDAPRecord. + """ + return ADLDAPRecord( + sam_account_name=entry.sAMAccountName.value, + mail=entry.mail.value, + description=entry.description.value, + sn=entry.sn.value, + given_name=entry.givenName.value, + ) + + def to_uwldap_record(self) -> UWLDAPRecord: + """Converts this AD LDAP record into a UW LDAP record.""" + return UWLDAPRecord( + uid=self.sam_account_name, + mail_local_addresses=[self.mail], + program=None, + # The description attribute has the format "Lastname, Firstname" + cn=' '.join(self.description.split(', ')[::-1]), + # Alumni's last names are prepended with an asterisk + sn=(self.sn[1:] if self.sn[0] == '*' else self.sn), + given_name=self.given_name + ) diff --git a/ceod/model/UWLDAPRecord.py b/ceo_common/model/UWLDAPRecord.py similarity index 100% rename from ceod/model/UWLDAPRecord.py rename to ceo_common/model/UWLDAPRecord.py diff --git a/ceo_common/model/__init__.py b/ceo_common/model/__init__.py index 14967e6..2ccb48d 100644 --- a/ceo_common/model/__init__.py +++ b/ceo_common/model/__init__.py @@ -1,4 +1,6 @@ +from .ADLDAPRecord import ADLDAPRecord from .Config import Config from .HTTPClient import HTTPClient from .RemoteMailmanService import RemoteMailmanService from .Term import Term +from .UWLDAPRecord import UWLDAPRecord diff --git a/ceo_common/utils.py b/ceo_common/utils.py index f126e56..b332b0c 100644 --- a/ceo_common/utils.py +++ b/ceo_common/utils.py @@ -1,6 +1,7 @@ import datetime import re from dataclasses import dataclass +import socket # TODO: disallow underscores. Will break many tests with usernames that include _ VALID_USERNAME_RE = re.compile(r"^[a-z][a-z0-9-_]+$") @@ -70,3 +71,8 @@ def validate_username(username: str) -> UsernameValidationResult: if not VALID_USERNAME_RE.fullmatch(username): return UsernameValidationResult(False, 'Username is invalid') return UsernameValidationResult(True) + + +def is_in_development() -> bool: + """This is a hack to determine if we're in the dev env or not""" + return socket.getfqdn().endswith('.csclub.internal') diff --git a/ceod/api/app_factory.py b/ceod/api/app_factory.py index 01890e5..6797e6c 100644 --- a/ceod/api/app_factory.py +++ b/ceod/api/app_factory.py @@ -9,13 +9,13 @@ from .error_handlers import register_error_handlers from ceo_common.interfaces import IConfig, IKerberosService, ILDAPService, IFileService, \ IMailmanService, IMailService, IUWLDAPService, IHTTPClient, IDatabaseService, \ ICloudStackService, ICloudResourceManager, IKubernetesService, IVHostManager, \ - IContainerRegistryService, IClubWebHostingService + IContainerRegistryService, IClubWebHostingService, IADLDAPService from ceo_common.model import Config, HTTPClient, RemoteMailmanService from ceod.api.spnego import init_spnego from ceod.model import KerberosService, LDAPService, FileService, \ MailmanService, MailService, UWLDAPService, CloudStackService, \ CloudResourceManager, KubernetesService, VHostManager, \ - ContainerRegistryService, ClubWebHostingService + ContainerRegistryService, ClubWebHostingService, ADLDAPService from ceod.db import MySQLService, PostgreSQLService @@ -36,6 +36,15 @@ def create_app(flask_config={}): from ceod.api import members app.register_blueprint(members.bp, url_prefix='/api/members') + from ceod.api import groups + app.register_blueprint(groups.bp, url_prefix='/api/groups') + + from ceod.api import positions + app.register_blueprint(positions.bp, url_prefix='/api/positions') + + from ceod.api import uwldap + app.register_blueprint(uwldap.bp, url_prefix='/api/uwldap') + # Only offer mailman API if this host is running Mailman if hostname == cfg.get('ceod_mailman_host'): from ceod.api import mailman @@ -53,15 +62,6 @@ def create_app(flask_config={}): from ceod.api import webhosting app.register_blueprint(webhosting.bp, url_prefix='/api/webhosting') - from ceod.api import groups - app.register_blueprint(groups.bp, url_prefix='/api/groups') - - from ceod.api import positions - app.register_blueprint(positions.bp, url_prefix='/api/positions') - - from ceod.api import uwldap - app.register_blueprint(uwldap.bp, url_prefix='/api/uwldap') - register_error_handlers(app) @app.route('/ping') @@ -112,9 +112,13 @@ def register_services(app): mail_srv = MailService() component.provideUtility(mail_srv, IMailService) - # UWLDAPService - uwldap_srv = UWLDAPService() - component.provideUtility(uwldap_srv, IUWLDAPService) + # UWLDAPService, ADLDAPService + if hostname == cfg.get('ceod_admin_host'): + uwldap_srv = UWLDAPService() + component.provideUtility(uwldap_srv, IUWLDAPService) + + adldap_srv = ADLDAPService() + component.provideUtility(adldap_srv, IADLDAPService) # ClubWebHostingService if hostname == cfg.get('ceod_webhosting_host'): diff --git a/ceod/api/uwldap.py b/ceod/api/uwldap.py index 7912a3f..9fd0268 100644 --- a/ceod/api/uwldap.py +++ b/ceod/api/uwldap.py @@ -3,15 +3,25 @@ from flask.json import jsonify from zope import component from .utils import authz_restrict_to_syscom, is_truthy -from ceo_common.interfaces import IUWLDAPService, ILDAPService +from ceo_common.interfaces import IUWLDAPService, IADLDAPService, ILDAPService +from ceo_common.logger_factory import logger_factory bp = Blueprint('uwldap', __name__) +logger = logger_factory(__name__) @bp.route('/') def get_user(username: str): uwldap_srv = component.getUtility(IUWLDAPService) record = uwldap_srv.get_user(username) + if record is not None and not record.sn: + # Alumni are missing a lot of information in UWLDAP. + # Try AD LDAP instead + logger.debug('Querying %s from AD LDAP', username) + adldap_srv = component.getUtility(IADLDAPService) + ad_record = adldap_srv.get_user(username) + if ad_record is not None: + record = ad_record.to_uwldap_record() if record is None: return { 'error': 'user not found', diff --git a/ceod/model/ADLDAPService.py b/ceod/model/ADLDAPService.py new file mode 100644 index 0000000..c3f9231 --- /dev/null +++ b/ceod/model/ADLDAPService.py @@ -0,0 +1,57 @@ +from typing import Optional + +import dns.resolver +import ldap3 +from zope import component +from zope.interface import implementer + +from ceo_common.interfaces import IADLDAPService, IConfig +from ceo_common.logger_factory import logger_factory +from ceo_common.model import ADLDAPRecord +from ceo_common.utils import is_in_development + +logger = logger_factory(__name__) + + +@implementer(IADLDAPService) +class ADLDAPService: + def __init__(self): + cfg = component.getUtility(IConfig) + if is_in_development(): + self.adldap_dns_srv_name = None + self.adldap_server_url = cfg.get('adldap_server_url') + else: + self.adldap_dns_srv_name = cfg.get('adldap_dns_srv_name') + # Perform the actual DNS query later so that we don't delay startup + self.adldap_server_url = None + self.adldap_base = cfg.get('adldap_base') + + def _get_server_url(self) -> str: + assert self.adldap_dns_srv_name is not None + answers = dns.resolver.resolve(self.adldap_dns_srv_name, 'SRV') + target = answers[0].target.to_text() + # Strip the trailing '.' + target = target[:-1] + logger.debug('Using AD LDAP server %s', target) + # ldaps doesn't seem to work + return 'ldap://' + target + + def _get_conn(self) -> ldap3.Connection: + if self.adldap_server_url is None: + self.adldap_server_url = self._get_server_url() + # When get_info=ldap3.SCHEMA (the default), ldap3 tries to search + # for schema information in 'CN=Schema,CN=Configuration,DC=ds,DC=uwaterloo,DC=ca', + # which doesn't exist so a LDAPNoSuchObjectResult exception is raised. + # To avoid this, we tell ldap3 not to look up any server info. + server = ldap3.Server(self.adldap_server_url, get_info=ldap3.NONE) + return ldap3.Connection( + server, auto_bind=True, read_only=True, raise_exceptions=True) + + def get_user(self, username: str) -> Optional[ADLDAPRecord]: + conn = self._get_conn() + conn.search( + self.adldap_base, f'(sAMAccountName={username})', + attributes=ADLDAPRecord.ldap_attributes, size_limit=1) + if not conn.entries: + return None + return ADLDAPRecord.deserialize_from_ldap(conn.entries[0]) diff --git a/ceod/model/UWLDAPService.py b/ceod/model/UWLDAPService.py index 38bbe4d..5d1ee46 100644 --- a/ceod/model/UWLDAPService.py +++ b/ceod/model/UWLDAPService.py @@ -1,11 +1,11 @@ -from typing import Union, List +from typing import List, Optional import ldap3 from zope import component from zope.interface import implementer -from .UWLDAPRecord import UWLDAPRecord from ceo_common.interfaces import IUWLDAPService, IConfig +from ceo_common.model import UWLDAPRecord @implementer(IUWLDAPService) @@ -20,7 +20,7 @@ class UWLDAPService: self.uwldap_server_url, auto_bind=True, read_only=True, raise_exceptions=True) - def get_user(self, username: str) -> Union[UWLDAPRecord, None]: + def get_user(self, username: str) -> Optional[UWLDAPRecord]: conn = self._get_conn() conn.search( self.uwldap_base, f'(uid={username})', @@ -29,7 +29,7 @@ class UWLDAPService: return None return UWLDAPRecord.deserialize_from_ldap(conn.entries[0]) - def get_programs_for_users(self, usernames: List[str]) -> List[Union[str, None]]: + def get_programs_for_users(self, usernames: List[str]) -> List[Optional[str]]: filter_str = '(|' + ''.join([f'(uid={uid})' for uid in usernames]) + ')' programs = [None] * len(usernames) user_indices = {uid: i for i, uid in enumerate(usernames)} diff --git a/ceod/model/__init__.py b/ceod/model/__init__.py index dd54b8e..df4ed93 100644 --- a/ceod/model/__init__.py +++ b/ceod/model/__init__.py @@ -1,3 +1,4 @@ +from .ADLDAPService import ADLDAPService from .CloudResourceManager import CloudResourceManager from .CloudStackService import CloudStackService from .KerberosService import KerberosService @@ -5,7 +6,6 @@ from .LDAPService import LDAPService, UserNotFoundError, GroupNotFoundError from .User import User from .Group import Group from .UWLDAPService import UWLDAPService -from .UWLDAPRecord import UWLDAPRecord from .FileService import FileService from .SudoRole import SudoRole from .MailService import MailService diff --git a/docker-compose.yml b/docker-compose.yml index d985398..b81a155 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,15 +1,16 @@ version: "3.6" x-common: &common - image: ceo-generic:bullseye volumes: - - ceo-venv:/app/venv:ro - ./.drone:/app/.drone:ro - ./docker-entrypoint.sh:/app/docker-entrypoint.sh:ro + - ceo-venv:/app/venv:ro - ./ceo:/app/ceo:ro - ./ceo_common:/app/ceo_common:ro - ./ceod:/app/ceod:ro - ./tests:/app/tests:ro + # for flake8 + - ./setup.cfg:/app/setup.cfg:ro security_opt: - label:disable working_dir: /app @@ -18,6 +19,7 @@ x-common: &common x-ceod-common: &ceod-common <<: *common + image: ceo-generic:bullseye environment: FLASK_APP: ceod.api FLASK_DEBUG: "true" diff --git a/etc/ceod.ini b/etc/ceod.ini index f378477..928f176 100644 --- a/etc/ceod.ini +++ b/etc/ceod.ini @@ -30,6 +30,10 @@ sudo_base = ou=SUDOers,dc=csclub,dc=uwaterloo,dc=ca server_url = ldaps://uwldap.uwaterloo.ca base = dc=uwaterloo,dc=ca +[adldap] +dns_srv_name = _ldap._tcp.teaching.ds.uwaterloo.ca +base = dc=teaching,dc=ds,dc=uwaterloo,dc=ca + [members] min_id = 20001 max_id = 29999 diff --git a/requirements.txt b/requirements.txt index 4159ec5..a595472 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ click==8.1.6 cryptography==41.0.2 +dnspython==2.5.0 Flask==2.3.2 gssapi==1.8.2 gunicorn==21.2.0 diff --git a/scripts/build-all-images.sh b/scripts/build-all-images.sh index dfca570..fa5526d 100755 --- a/scripts/build-all-images.sh +++ b/scripts/build-all-images.sh @@ -35,6 +35,7 @@ if ! $DOCKER volume exists ceo-venv; then $DOCKER volume create ceo-venv $DOCKER run \ --rm \ + --security-opt label=disable \ -w /app/venv \ -v ceo-venv:/app/venv:z \ -v ./requirements.txt:/tmp/requirements.txt:ro \ diff --git a/tests/ceod/api/test_uwldap.py b/tests/ceod/api/test_uwldap.py index d4994e5..7b18910 100644 --- a/tests/ceod/api/test_uwldap.py +++ b/tests/ceod/api/test_uwldap.py @@ -50,3 +50,19 @@ def test_updateprograms( # make sure that the user was changed status, data = client.get(f'/api/members/{uwldap_user.uid}') assert data['program'] == 'New Program' + + +def test_get_alumni(client): + uid = 'alumni1' + status, data = client.get(f'/api/uwldap/{uid}') + assert status == 200 + expected = { + "cn": 'Alumni One', + "given_name": 'Alumni', + "sn": 'One', + # This should be the email address from AD LDAP + "mail_local_addresses": [f'{uid}@alumni.uwaterloo.internal'], + # Program attribute should be missing + "uid": uid, + } + assert data == expected diff --git a/tests/ceod_dev.ini b/tests/ceod_dev.ini index bf529b5..fbbcfbd 100644 --- a/tests/ceod_dev.ini +++ b/tests/ceod_dev.ini @@ -27,6 +27,10 @@ sudo_base = ou=SUDOers,dc=csclub,dc=internal server_url = ldap://auth1.csclub.internal base = ou=UWLDAP,dc=csclub,dc=internal +[adldap] +server_url = ldap://auth1.csclub.internal +base = ou=ADLDAP,dc=csclub,dc=internal + [members] min_id = 20001 max_id = 29999 diff --git a/tests/ceod_test_local.ini b/tests/ceod_test_local.ini index f6e84da..39962b8 100644 --- a/tests/ceod_test_local.ini +++ b/tests/ceod_test_local.ini @@ -26,6 +26,10 @@ sudo_base = ou=TestSUDOers,dc=csclub,dc=internal server_url = ldap://auth1.csclub.internal base = ou=TestUWLDAP,dc=csclub,dc=internal +[adldap] +server_url = ldap://auth1.csclub.internal +base = ou=TestADLDAP,dc=csclub,dc=internal + [members] # 20000 is ctdalek, 20001 is office1 min_id = 20002 diff --git a/tests/conftest.py b/tests/conftest.py index 48be560..1374574 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -30,15 +30,16 @@ from .utils import ( # noqa: F401 from ceo_common.interfaces import IConfig, IKerberosService, ILDAPService, \ IFileService, IMailmanService, IHTTPClient, IUWLDAPService, IMailService, \ IDatabaseService, ICloudStackService, IKubernetesService, IVHostManager, \ - ICloudResourceManager, IContainerRegistryService, IClubWebHostingService -from ceo_common.model import Config, HTTPClient, Term + ICloudResourceManager, IContainerRegistryService, IClubWebHostingService, \ + IADLDAPService +from ceo_common.model import Config, HTTPClient, Term, UWLDAPRecord import ceo_common.utils from ceod.api import create_app from ceod.db import MySQLService, PostgreSQLService from ceod.model import KerberosService, LDAPService, FileService, User, \ - MailmanService, Group, UWLDAPService, UWLDAPRecord, MailService, \ + MailmanService, Group, UWLDAPService, MailService, \ CloudStackService, KubernetesService, VHostManager, CloudResourceManager, \ - ContainerRegistryService, ClubWebHostingService + ContainerRegistryService, ClubWebHostingService, ADLDAPService from .MockSMTPServer import MockSMTPServer from .MockMailmanServer import MockMailmanServer from .MockCloudStackServer import MockCloudStackServer @@ -309,6 +310,17 @@ def uwldap_srv(cfg, ldap_conn): 'givenName': 'Calum', }, ) + conn.add( + f'uid=alumni1,{base_dn}', + ['inetLocalMailRecipient', 'inetOrgPerson', 'organizationalPerson', 'person'], + { + 'mailLocalAddress': 'alumni1@uwaterloo.internal', + 'ou': 'Alumni', + 'cn': 'Alumni', + 'sn': 'One', + 'givenName': 'Alumni', + }, + ) _uwldap_srv = UWLDAPService() component.getGlobalSiteManager().registerUtility(_uwldap_srv, IUWLDAPService) yield _uwldap_srv @@ -316,6 +328,31 @@ def uwldap_srv(cfg, ldap_conn): delete_subtree(conn, base_dn) +@pytest.fixture(scope='session') +def adldap_srv(cfg, ldap_conn): + conn = ldap_conn + base_dn = cfg.get('adldap_base') + delete_subtree(conn, base_dn) + conn.add(base_dn, 'organizationalUnit') + conn.add( + f'cn=alumni1,{base_dn}', + ['mockADUser', 'inetOrgPerson', 'organizationalPerson', 'person'], + { + 'description': 'One, Alumni', + 'givenName': 'Alumni', + 'sn': '*One', + 'cn': 'alumni1', + 'sAMAccountName': 'alumni1', + 'displayName': 'alumni1', + 'mail': 'alumni1@alumni.uwaterloo.internal', + }, + ) + _adldap_srv = ADLDAPService() + component.getGlobalSiteManager().registerUtility(_adldap_srv, IADLDAPService) + yield _adldap_srv + delete_subtree(conn, base_dn) + + @pytest.fixture(scope='session') def webhosting_srv(): with tempfile.TemporaryDirectory() as tmpdir: @@ -376,14 +413,25 @@ def postgresql_srv(cfg): return psql_srv +def delete_dir_contents(dir: str): + if os.path.isdir(dir): + for item in os.listdir(dir): + path = os.path.join(dir, item) + if os.path.isdir(path): + shutil.rmtree(path) + else: + os.unlink(path) + + @pytest.fixture(scope='session') def vhost_dir_setup(cfg): state_dir = '/run/ceod' - if os.path.isdir(state_dir): - shutil.rmtree(state_dir) - os.makedirs(state_dir) + # Don't delete the directory itself because the non-test ceod process + # is using it + delete_dir_contents(state_dir) + os.makedirs(state_dir, exist_ok=True) yield - shutil.rmtree(state_dir) + delete_dir_contents(state_dir) @pytest.fixture(scope='session') @@ -422,6 +470,7 @@ def app( file_srv, mailman_srv, uwldap_srv, + adldap_srv, mail_srv, mysql_srv, postgresql_srv,