Query Active Directory LDAP for alumni (#120)
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
Closes #116. UWLDAP has program information for current students, so we should continue using it by default. If the sn attribute (last name) is missing from the entry, then we query ADLDAP instead. Reviewed-on: #120
This commit is contained in:
parent
bd1da799c6
commit
a4a4ef089c
|
@ -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
|
|
@ -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 || \
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ) )
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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.
|
||||
"""
|
|
@ -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
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
from .IADLDAPService import IADLDAPService
|
||||
from .ICloudResourceManager import ICloudResourceManager
|
||||
from .ICloudStackService import ICloudStackService
|
||||
from .IKerberosService import IKerberosService
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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'):
|
||||
|
|
|
@ -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('/<username>')
|
||||
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',
|
||||
|
|
|
@ -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])
|
|
@ -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)}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 \
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue