add MailService and MailmanService

pull/5/head
Max Erenberg 1 year ago
parent de0f473881
commit 3b78b7ffb4
  1. 4
      .gbp.conf
  2. 8
      .gitignore
  3. 40
      bin/ceo
  4. 5
      build.sh
  5. 24
      ceo_common/interfaces/IFileService.py
  6. 4
      ceo_common/interfaces/IGroup.py
  7. 11
      ceo_common/interfaces/ILDAPService.py
  8. 15
      ceo_common/interfaces/IMailService.py
  9. 11
      ceo_common/interfaces/IMailmanService.py
  10. 15
      ceo_common/interfaces/IUWLDAPService.py
  11. 18
      ceo_common/interfaces/IUser.py
  12. 4
      ceo_common/interfaces/__init__.py
  13. 13
      ceo_common/model/Config.py
  14. 79
      ceod/model/FileService.py
  15. 4
      ceod/model/Group.py
  16. 13
      ceod/model/LDAPService.py
  17. 63
      ceod/model/MailService.py
  18. 51
      ceod/model/MailmanService.py
  19. 31
      ceod/model/SudoRole.py
  20. 30
      ceod/model/UWLDAPRecord.py
  21. 23
      ceod/model/UWLDAPService.py
  22. 35
      ceod/model/User.py
  23. 6
      ceod/model/__init__.py
  24. 44
      ceod/model/templates/welcome_message.j2
  25. 52
      ceod/model/validators.py
  26. 9
      debian/.gitignore
  27. 2
      debian/ceo-clients.manpages
  28. 1
      debian/ceo-common.dirs
  29. 1
      debian/ceo-common.install
  30. 55
      debian/ceo-daemon.ceod.init
  31. 1
      debian/ceo-daemon.dirs
  32. 1
      debian/ceo-daemon.install
  33. 1
      debian/ceo-daemon.manpages
  34. 1
      debian/ceo-python.manpages
  35. 739
      debian/changelog
  36. 1
      debian/compat
  37. 42
      debian/control
  38. 31
      debian/copyright
  39. 52
      debian/rules
  40. 55
      docs/GIT-HOWTO
  41. 78
      docs/INSTALLING
  42. 13
      docs/addclub.1
  43. 16
      docs/addmember.1
  44. 17
      docs/ceo.1
  45. 13
      docs/ceod.8
  46. 48
      etc/accounts.cf
  47. 35
      etc/csc.schema
  48. 2
      etc/mailman.cf
  49. 1
      etc/ops/adduser
  50. 1
      etc/ops/mail
  51. 1
      etc/ops/mailman
  52. 1
      etc/ops/mysql
  53. 50
      etc/spam/expired-account
  54. 13
      etc/spam/new-member
  55. 70
      etc/spam/new-member.d/announce
  56. 49
      misc/expired-account
  57. 71
      misc/notify-hook
  58. 11
      setup.py
  59. 10
      setupd.py
  60. 14
      src/.gitignore
  61. 86
      src/Makefile
  62. 115
      src/addclub.c
  63. 131
      src/addmember.c
  64. 43
      src/ceo.proto
  65. 167
      src/ceoc.c
  66. 21
      src/config-test.c
  67. 24
      src/config-vars.h
  68. 91
      src/config.c
  69. 10
      src/config.h
  70. 7
      src/daemon.h
  71. 218
      src/dmaster.c
  72. 149
      src/dslave.c
  73. 307
      src/gss.c
  74. 15
      src/gss.h
  75. 171
      src/homedir.c
  76. 6
      src/homedir.h
  77. 97
      src/kadm.c
  78. 5
      src/kadm.h
  79. 132
      src/krb5.c
  80. 14
      src/krb5.h
  81. 383
      src/ldap.c
  82. 12
      src/ldap.h
  83. 95
      src/net.c
  84. 24
      src/net.h
  85. 335
      src/op-adduser.c
  86. 217
      src/op-mail.c
  87. 47
      src/op-mailman
  88. 99
      src/op-mysql
  89. 128
      src/ops.c
  90. 15
      src/ops.h
  91. 215
      src/parser.c
  92. 2
      src/parser.h
  93. 458
      src/strbuf.c
  94. 158
      src/strbuf.h
  95. 305
      src/util.c
  96. 89
      src/util.h

@ -1,4 +0,0 @@
[DEFAULT]
sign-tags = True
posttag = git push /users/git/public/pyceo.git --tags
debian-tag=v%(version)s

8
.gitignore vendored

@ -1,5 +1,3 @@
/build-stamp
/build
*.pyc
/build-ceo
/build-ceod
__pycache__/
/venv/
.vscode/

@ -1,40 +0,0 @@
#!/usr/bin/python
import sys, ldap
from getpass import getpass
import ceo.urwid.main
import ceo.console.main
from ceo import ldapi, members
def start():
try:
if len(sys.argv) == 1:
print "Reading config file...",
members.configure()
print "Connecting to LDAP..."
members.connect(AuthCallback())
ceo.urwid.main.start()
else:
members.configure()
members.connect(AuthCallback())
ceo.console.main.start()
except ldap.LOCAL_ERROR, e:
print ldapi.format_ldaperror(e)
except ldap.INSUFFICIENT_ACCESS, e:
print ldapi.format_ldaperror(e)
print "You probably aren't permitted to do whatever you just tried."
print "Admittedly, ceo probably shouldn't have crashed either."
class AuthCallback:
def callback(self, error):
try:
print "Password: ",
return getpass("")
except KeyboardInterrupt:
print ""
sys.exit(1)
if __name__ == '__main__':
start()

@ -1,5 +0,0 @@
if test -e .git; then
git-buildpackage --git-ignore-new -us -uc
else
debuild -us -uc
fi

@ -0,0 +1,24 @@
from typing import List
from zope.interface import Interface
class IFileService(Interface):
"""
A service which can access, create and modify files on the
NFS users' directory.
"""
def create_home_dir(username: str, is_club: bool = False):
"""
Create a new home dir for the given user or club.
"""
def get_forwarding_addresses(username: str) -> List[str]:
"""
Get the contents of the user's ~/.forward file,
one line at a time.
"""
def set_forwarding_addresses(username: str, addresses: List[str]):
"""Set the contents of the user's ~/.forward file."""

@ -25,14 +25,14 @@ class IGroup(Interface):
def get_members() -> List[IUser]:
"""Get a list of the members in this group."""
def serialize_for_modlist() -> Dict:
def serialize_for_modlist() -> Dict[str, List[bytes]]:
"""
Serialize this group into a dict to be passed to
ldap.modlist.addModlist().
"""
# static method
def deserialize_from_dict(data: Dict):
def deserialize_from_dict(data: Dict[str, List[bytes]]):
"""Deserialize this group from a dict returned by ldap.search_s().
:returns: IGroup

@ -10,18 +10,18 @@ class ILDAPService(Interface):
def get_user(username: str) -> IUser:
"""Retrieve the user with the given username."""
def save_user(user: IUser) -> IUser:
def add_user(user: IUser) -> IUser:
"""
Save the user in the database.
Add the user to the database.
A new UID and GID will be generated and returned in the new user.
"""
def get_group(cn: str, is_club: bool = False) -> IGroup:
"""Retrieve the group with the given cn (Unix group name)."""
def save_group(group: IGroup) -> IGroup:
def add_group(group: IGroup) -> IGroup:
"""
Save the group in the database.
Add the group to the database.
The GID will not be changed and must be valid.
"""
@ -30,3 +30,6 @@ class ILDAPService(Interface):
def modify_group(old_group: IGroup, new_group: IGroup):
"""Replace old_group with new_group."""
def add_sudo_role(uid: str):
"""Create a sudo role for the club with this UID."""

@ -0,0 +1,15 @@
from typing import Dict
from zope.interface import Interface
from .IUser import IUser
class IMailService(Interface):
"""An interface to send email messages."""
def send(_from: str, to: str, headers: Dict[str, str], content: str):
"""Send a message with the given headers and content."""
def send_welcome_message_to(user: IUser):
"""Send a welcome message to the new member."""

@ -0,0 +1,11 @@
from zope.interface import Interface
class IMailmanService(Interface):
"""A service to susbcribe and unsubscribe people from mailing lists."""
def subscribe(address: str, mailing_list: str):
"""Subscribe the email address to the mailing list."""
def unsubscribe(address: str, mailing_list: str):
"""Unsubscribe the email address from the mailing list."""

@ -0,0 +1,15 @@
from typing import Union
from zope.interface import Interface
class IUWLDAPService(Interface):
"""Represents the UW LDAP database."""
def get(username: str):
"""
Return the LDAP record for the given user, or
None if no such record exists.
:rtype: Union[UWLDAPRecord, None]
"""

@ -24,6 +24,12 @@ class IUser(Interface):
# Non-LDAP attributes
forwarding_addresses = Attribute('list of email forwarding addresses')
def get_forwarding_addresses(self) -> List[str]:
"""Get the forwarding addresses for this user."""
def set_forwarding_addresses(self, addresses: List[str]):
"""Set the forwarding addresses for this user."""
def is_club() -> bool:
"""
Returns True if this is the Unix user for a club.
@ -31,7 +37,10 @@ class IUser(Interface):
"""
def add_to_ldap():
"""Add a new record to LDAP for this user."""
"""
Add a new record to LDAP for this user.
A new UID number and GID number will be created.
"""
def add_to_kerberos(password: str):
"""Add a new Kerberos principal for this user."""
@ -51,14 +60,17 @@ class IUser(Interface):
def change_password(password: str):
"""Replace the user's password."""
def serialize_for_modlist() -> Dict:
def create_home_dir():
"""Create a new home directory for this user."""
def serialize_for_modlist() -> Dict[str, List[bytes]]:
"""
Serialize this user into a dict to be passed to
ldap.modlist.addModlist().
"""
# static method
def deserialize_from_dict(data: Dict):
def deserialize_from_dict(data: Dict[str, List[bytes]]):
"""Deserialize this user from a dict returned by ldap.search_s().
:returns: IUser

@ -3,3 +3,7 @@ from .IConfig import IConfig
from .IUser import IUser
from .ILDAPService import ILDAPService
from .IGroup import IGroup
from .IFileService import IFileService
from .IUWLDAPService import IUWLDAPService
from .IMailService import IMailService
from .IMailmanService import IMailmanService

@ -9,15 +9,28 @@ class Config:
_domain = 'csclub.internal'
_ldap_base = ','.join(['dc=' + dc for dc in _domain.split('.')])
_config = {
'base_domain': _domain,
'ldap_admin_principal': 'ceod/admin',
'ldap_server_url': 'ldap://ldap-master.' + _domain,
'ldap_users_base': 'ou=People,' + _ldap_base,
'ldap_groups_base': 'ou=Group,' + _ldap_base,
'ldap_sudo_base': 'ou=SUDOers,' + _ldap_base,
'ldap_sasl_realm': _domain.upper(),
'uwldap_server_url': 'ldap://uwldap.uwaterloo.ca',
'uwldap_base': 'dc=uwaterloo,dc=ca',
'member_min_id': 20001,
'member_max_id': 29999,
'club_min_id': 30001,
'club_max_id': 39999,
'member_home': '/users',
'club_home': '/users',
'member_home_skel': '/users/skel',
'club_home_skel': '/users/skel',
'smtp_url': 'smtp://mail.' + _domain,
'smtp_starttls': False,
'mailman3_api_base_url': 'http://localhost:8001/3.1',
'mailman3_api_username': 'restadmin',
'mailman3_api_password': 'mailman3',
}
def get(self, key: str) -> str:

@ -0,0 +1,79 @@
import os
import pwd
import shutil
from typing import List
from zope import component
from zope.interface import implementer
from .validators import is_valid_forwarding_address, InvalidForwardingAddressException
from ceo_common.interfaces import IFileService, IConfig
@implementer(IFileService)
class FileService:
def __init__(self):
cfg = component.getUtility(IConfig)
self.member_home_skel = cfg.get('member_home_skel')
self.club_home_skel = cfg.get('club_home_skel')
def create_home_dir(self, username: str, is_club: bool = False):
if is_club:
skel_dir = self.club_home_skel
else:
skel_dir = self.member_home_skel
pwnam = pwd.getpwnam(username)
home = pwnam.pw_dir
uid = pwnam.pw_uid
gid = pwnam.pw_gid
# recursively copy skel dir to user's home
shutil.copytree(skel_dir, home)
# Set ownership and permissions on user's home.
# The setgid bit ensures that all files created under that
# directory belong to the owner (useful for clubs).
os.chmod(home, mode=0o2751) # rwxr-s--x
os.chown(home, uid=uid, gid=gid)
# recursively set file ownership
for root, dirs, files in os.walk(home):
for dir in dirs:
os.chown(os.path.join(root, dir), uid=uid, gid=gid)
for file in files:
os.chown(os.path.join(root, file), uid=uid, gid=gid)
def get_forwarding_addresses(self, username: str) -> List[str]:
pwnam = pwd.getpwnam(username)
home = pwnam.pw_dir
forward_file = os.path.join(home, '.forward')
if not os.path.isfile(forward_file):
return []
lines = [
line.strip() for line in open(forward_file).readlines()
]
return [
line for line in lines
if line != '' and line[0] != '#'
]
def set_forwarding_addresses(self, username: str, addresses: List[str]):
for line in addresses:
if not is_valid_forwarding_address(line):
raise InvalidForwardingAddressException(line)
pwnam = pwd.getpwnam(username)
home = pwnam.pw_dir
uid = pwnam.pw_uid
gid = pwnam.pw_gid
forward_file = os.path.join(home, '.forward')
if os.path.exists(forward_file):
# create a backup
backup_forward_file = forward_file + '.bak'
shutil.copyfile(forward_file, backup_forward_file)
os.chown(backup_forward_file, uid=uid, gid=gid)
else:
# create a new ~/.forward file
open(forward_file, 'w')
os.chown(forward_file, uid=uid, gid=gid)
with open(forward_file, 'w') as f:
for line in addresses:
f.write(line + '\n')

@ -40,7 +40,7 @@ class Group:
def add_to_ldap(self):
self.ldap_srv.add_group(self)
def serialize_for_modlist(self) -> Dict:
def serialize_for_modlist(self) -> Dict[str, List[bytes]]:
data = {
'cn': [self.cn],
'gidNumber': [str(self.gid_number)],
@ -55,7 +55,7 @@ class Group:
return strings_to_bytes(data)
@staticmethod
def deserialize_from_dict(data: Dict) -> IGroup:
def deserialize_from_dict(data: Dict[str, List[bytes]]) -> IGroup:
data = bytes_to_strings(data)
return Group(
cn=data['cn'][0],

@ -10,6 +10,7 @@ from zope.interface import implementer
from ceo_common.interfaces import ILDAPService, IKerberosService, IConfig, IUser, IGroup
from .User import User
from .Group import Group
from .SudoRole import SudoRole
class UserNotFoundError:
@ -34,6 +35,7 @@ class LDAPService:
self.club_max_id = cfg.get('club_max_id')
def _get_ldap_conn(self, gssapi_bind: bool = True) -> ldap.ldapobject.LDAPObject:
# TODO: cache the connection
conn = ldap.initialize(self.ldap_server_url)
if gssapi_bind:
self._gssapi_bind(conn)
@ -99,7 +101,13 @@ class LDAPService:
return uid
raise Exception('no UIDs remaining')
def save_user(self, user: IUser) -> IUser:
def add_sudo_role(self, uid: str):
conn = self._get_ldap_conn()
sudo_role = SudoRole(uid)
modlist = ldap.modlist.addModlist(sudo_role.serialize_for_modlist())
conn.add_s(sudo_role.dn, modlist)
def add_user(self, user: IUser) -> IUser:
if user.is_club():
min_id, max_id = self.club_min_id, self.club_max_id
else:
@ -112,9 +120,10 @@ class LDAPService:
modlist = ldap.modlist.addModlist(new_user.serialize_for_modlist())
conn.add_s(new_user.dn, modlist)
return new_user
def save_group(self, group: IGroup) -> IGroup:
def add_group(self, group: IGroup) -> IGroup:
conn = self._get_ldap_conn()
# make sure that the caller initialized the GID number
assert group.gid_number

@ -0,0 +1,63 @@
import datetime
from email.message import EmailMessage
import re
import smtplib
from typing import Dict
import jinja2
from zope import component
from zope.interface import implementer
from ceo_common.interfaces import IMailService, IConfig, IUser
smtp_url_re = re.compile(r'^(?P<scheme>smtps?)://(?P<host>[\w.-]+)(:(?P<port>\d+))?$')
@implementer(IMailService)
class MailService:
def __init__(self):
cfg = component.getUtility(IConfig)
smtp_url = cfg.get('smtp_url')
match = smtp_url_re.match(smtp_url)
if match is None:
raise Exception('Invalid SMTP URL: %s' % smtp_url)
self.smtps = match.group('scheme') == 'smtps'
self.host = match.group('host')
self.port = int(match.group('port') or 25)
self.starttls = cfg.get('smtp_starttls')
assert not (self.smtps and self.starttls)
self.base_domain = cfg.get('base_domain')
self.jinja_env = jinja2.Environment(
loader=jinja2.PackageLoader('ceod.model'),
)
def send(self, _from: str, to: str, headers: Dict[str, str], content: str):
msg = EmailMessage()
msg.set_content(content)
msg['From'] = _from
msg['To'] = to
msg['Date'] = datetime.datetime.now().astimezone().strftime('%a, %d %b %Y %H:%M:%S %z')
for key, val in headers.items():
msg[key] = val
if self.smtps:
client = smtplib.SMTP_SSL(self.host, self.port)
else:
client = smtplib.SMTP(self.host, self.port)
client.ehlo()
if self.starttls:
client.starttls()
client.send_message(msg)
client.quit()
def send_welcome_message_to(self, user: IUser):
template = self.jinja_env.get_template('welcome_message.j2')
# TODO: store surname and givenName in LDAP
first_name = user.cn.split(' ', 1)[0]
body = template.render(name=first_name, user=user.uid)
self.send(
f'Computer Science Club <exec@{self.base_domain}>',
f'{user.cn} <{user.uid}@{self.base_domain}>',
{'Subject': 'Welcome to the Computer Science Club'},
body,
)

@ -0,0 +1,51 @@
import requests
from requests.auth import HTTPBasicAuth
from zope import component
from zope.interface import implementer
from ceo_common.interfaces import IMailmanService, IConfig
@implementer(IMailmanService)
class MailmanService:
def __init__(self):
cfg = component.getUtility(IConfig)
self.base_domain = cfg.get('base_domain')
self.api_base_url = cfg.get('mailman3_api_base_url')
self.api_username = cfg.get('mailman3_api_username')
self.api_password = cfg.get('mailman3_api_password')
def subscribe(self, address: str, mailing_list: str):
if '@' in mailing_list:
mailing_list = mailing_list[:mailing_list.index('@')]
if '@' not in address:
address = f'{address}@{self.base_domain}'
url = f'{self.api_base_url}/members'
resp = requests.post(
url,
data={
'list_id': f'{mailing_list}.{self.base_domain}',
'subscriber': address,
'pre_verified': 'True',
'pre_confirmed': 'True',
'pre_approved': 'True',
},
auth=HTTPBasicAuth(self.api_username, self.api_password),
)
resp.raise_for_status()
def unsubscribe(self, address: str, mailing_list: str):
if '@' not in mailing_list:
mailing_list = f'{mailing_list}@{self.base_domain}'
if '@' not in address:
address = f'{address}@{self.base_domain}'
url = f'{self.api_base_url}/lists/{mailing_list}/member/{address}'
resp = requests.delete(
url,
data={
'pre_approved': 'True',
'pre_confirmed': 'True',
},
auth=HTTPBasicAuth(self.api_username, self.api_password),
)
resp.raise_for_status()

@ -0,0 +1,31 @@
from zope import component
from .utils import strings_to_bytes
from ceo_common.interfaces import IConfig
class SudoRole:
"""Represents a sudoRole record in LDAP."""
def __init__(self, uid: str):
cfg = component.getUtility(IConfig)
ldap_sudo_base = cfg.get('ldap_sudo_base')
self.uid = uid
self.dn = f'cn=%{uid},{ldap_sudo_base}'
def serialize_for_modlist(self):
# TODO: use sudoOrder
data = {
'objectClass': [
'top',
'sudoRole',
],
'cn': '%' + self.uid,
'sudoUser': '%' + self.uid,
'sudoHost': 'ALL',
'sudoCommand': 'ALL',
'sudoOption': '!authenticate',
'sudoRunAsUser': self.uid,
}
return strings_to_bytes(data)

@ -0,0 +1,30 @@
from typing import List, Dict, Union
from .utils import bytes_to_strings
class UWLDAPRecord:
"""Represents a record from the UW LDAP."""
def __init__(
self,
uid: str,
program: Union[str, None],
mail_local_addresses: List[str],
):
self.uid = uid
self.program = program
self.mail_local_addresses = mail_local_addresses
@staticmethod
def deserialize_from_dict(self, data: Dict[str, List[bytes]]):
"""
Deserializes a dict returned from ldap.search_s() into a
UWLDAPRecord.
"""
data = bytes_to_strings(data)
return UWLDAPRecord(
uid=data['uid'][0],
program=data.get('ou', [None])[0],
mail_local_addresses=data['mailLocalAddress'],
)

@ -0,0 +1,23 @@
from typing import Union
import ldap
from zope import component
from zope.interface import implementer
from .UWLDAPRecord import UWLDAPRecord
from ceo_common.interfaces import IUWLDAPService, IConfig
@implementer(IUWLDAPService)
class UWLDAPService:
def __init__(self):
cfg = component.getUtility(IConfig)
self.uwldap_server_url = cfg.get('uwldap_server_url')
self.uwldap_base = cfg.get('uwldap_base')
def get(self, username: str) -> Union[UWLDAPRecord, None]:
conn = ldap.initialize(self.uwldap_server_url)
results = conn.search_s(self.uwldap_base, ldap.SCOPE_SUBTREE, f'uid={username}')
if not results:
return None
return UWLDAPRecord.deserialize_from_dict(results[0])

@ -6,7 +6,8 @@ from zope import component
from zope.interface import implementer
from .utils import strings_to_bytes, bytes_to_strings
from ceo_common.interfaces import ILDAPService, IKerberosService, IUser, IConfig
from ceo_common.interfaces import ILDAPService, IKerberosService, IFileService, \
IUser, IConfig
@implementer(IUser)
@ -26,6 +27,8 @@ class User:
):
if not is_club and not terms and not non_member_terms:
raise Exception('terms and non_member_terms cannot both be empty')
cfg = component.getUtility(IConfig)
self.uid = uid
self.cn = cn
self.program = program
@ -34,20 +37,23 @@ class User:
self.login_shell = login_shell
self.uid_number = uid_number
self.gid_number = gid_number
self.home_directory = home_directory or os.path.join('/users', uid)
if home_directory is None:
if is_club:
home_parent = cfg.get('member_home')
else:
home_parent = cfg.get('club_home')
self.home_directory = os.path.join(home_parent, uid)
else:
self.home_directory = home_directory
self.positions = positions or []
self.mail_local_addresses = mail_local_addresses or []
self._is_club = is_club
cfg = component.getUtility(IConfig)
self.ldap_sasl_realm = cfg.get('ldap_sasl_realm')
self.dn = f'uid={uid},{cfg.get("ldap_users_base")}'
self.ldap_srv = component.getUtility(ILDAPService)
self.krb_srv = component.getUtility(IKerberosService)
@property
def forwarding_addresses(self):
raise NotImplementedError()
self.file_srv = component.getUtility(IFileService)
def __repr__(self) -> str:
lines = [
@ -80,7 +86,7 @@ class User:
return self._is_club
def add_to_ldap(self):
new_member = self.ldap_srv.save_user(self)
new_member = self.ldap_srv.add_user(self)
self.uid_number = new_member.uid_number
self.gid_number = new_member.gid_number
@ -90,6 +96,9 @@ class User:
def change_password(self, password: str):
self.krb_srv.change_password(self.uid, password)
def create_home_dir(self):
self.file_srv.create_home_dir(self.uid, self._is_club)
def serialize_for_modlist(self) -> Dict:
data = {
'cn': [self.cn],
@ -124,7 +133,7 @@ class User:
return strings_to_bytes(data)
@staticmethod
def deserialize_from_dict(data: Dict) -> IUser:
def deserialize_from_dict(data: Dict[str, List[bytes]]) -> IUser:
data = bytes_to_strings(data)
return User(
uid=data['uid'][0],
@ -167,3 +176,11 @@ class User:
new_user.positions.remove(position)
self.ldap_srv.modify_user(self, new_user)
self.positions = new_user.positions
def get_forwarding_addresses(self) -> List[str]:
return self.file_srv.get_forwarding_addresses(self.uid)
def set_forwarding_addresses(self, addresses: List[str]):
self.file_srv.set_forwarding_addresses(self.uid, addresses)
forwarding_addresses = property(get_forwarding_addresses, set_forwarding_addresses)

@ -2,3 +2,9 @@ from .KerberosService import KerberosService
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
from .MailmanService import MailmanService

@ -1,25 +1,4 @@
#!/bin/bash -p
# This is a privileged script.
IFS=$' \t\n'
PATH=/usr/bin:/bin
unset ENV BASH_ENV CDPATH
umask 077
prog=$CEO_PROG
auth=$CEO_AUTH
tmp="$(tempfile)"
trap "rm $tmp" 0
exec >"$tmp"
h_from="Computer Science Club <exec@csclub.uwaterloo.ca>"
h_to="$CEO_NAME <$CEO_USER@csclub.uwaterloo.ca>"
subj="Welcome to the Computer Science Club"
if test "$prog" = addmember; then
user="$CEO_USER" name="$CEO_NAME"
body="Hello $name:
Hello {{ name }}:
Welcome to the Computer Science Club! We are pleased that you have chosen to join us. We welcome you to come out to our events, or just hang out in our office (MC 3036/3037). You have been automatically subscribed to our mailing list, csc-general, which we use to keep you informed of upcoming events.
@ -40,17 +19,17 @@ You can hear about upcoming events in a number of ways:
Even when events aren't being held, you are welcome to hang out in the club office (MC 3036/3037, across the hall from MathSoc). It's often open late into the evening, and sells pop and snacks at reasonable prices. If you're so inclined, you are also welcome in our IRC channel, #csc on FreeNode.
You now have a CSC user account with username \"$user\" and the password you supplied when you joined. You can use this account to log into almost any CSC system, including our office terminals and servers. A complete list is available at:
You now have a CSC user account with username "{{ user }}" and the password you supplied when you joined. You can use this account to log into almost any CSC system, including our office terminals and servers. A complete list is available at:
http://wiki.csclub.uwaterloo.ca/Machine_List
You can connect remotely using SSH. On Windows, PuTTY is a popular SSH client; on Unix-like operating systems, you can connect with the 'ssh' command, like this:
ssh $user@corn-syrup.csclub.uwaterloo.ca
ssh {{ user }}@corn-syrup.csclub.uwaterloo.ca
To use CSC web hosting, simply place files in the 'www' directory in your home directory. Files placed there will be available at:
http://csclub.uwaterloo.ca/~$user/
http://csclub.uwaterloo.ca/~{{ user }}/
We support many server-side technologies, including PHP, Perl and Python. If you need a MySQL database, you can create one for yourself using the 'ceo' command-line tool.
@ -70,18 +49,3 @@ To contact the executive, email:
Regards,
Computer Science Club Executive
"
elif [[ "$prog" = addclubrep || "$prog" = addclub ]]; then
exit 0
else
exit 1
fi
echo "From: $h_from"
echo "To: $h_to"
echo "Subject: $subj"
echo
echo "$body" | fmt -s
exec >&2
env - /usr/sbin/sendmail -t -f "exec@csclub.uwaterloo.ca" < "$tmp"

@ -0,0 +1,52 @@
import inspect
import re
from typing import Callable
class InvalidUsernameException(Exception):
pass
class InvalidForwardingAddressException(Exception):
pass
valid_username_re = re.compile(r'^[a-z][\w-]+$')
# Only allow usernames and email addresses to be set in ~/.forward
valid_forwarding_address_re = re.compile(r'^[\w-]+|[\w.+-]+@[\w-]+(\.[\w-]+)*$')
def is_valid_username(username: str) -> bool:
"""Returns True if the username has a valid format."""
return valid_username_re.match(username) is not None
def check_valid_username(f: Callable) -> Callable:
"""
A decorator which raises an Exception if the username passed
to f is invalid.
f must accept `username` as a regular argument.
"""
argspec = inspect.getfullargspec(f)
try:
arg_idx = argspec.args.index('username')
except ValueError:
return f
def wrapper(*args, **kwargs):
username = None
if arg_idx < len(args):
username = args[arg_idx]
elif 'username' in kwargs:
username = kwargs['username']
if username is not None and not is_valid_username(username):
raise InvalidUsernameException(username)
return f(*args, **kwargs)
return wrapper
def is_valid_forwarding_address(address: str) -> bool:
"""Returns True if the address is a valid forwarding address."""
return valid_forwarding_address_re.match(address) is not None

9
debian/.gitignore vendored

@ -1,9 +0,0 @@
/ceo.substvars
/ceo-common
/ceo-clients
/ceo-daemon
/ceo-python
/files
/*.debhelper
/*.debhelper.log
/*.substvars

@ -1,2 +0,0 @@
docs/addclub.1
docs/addmember.1

@ -1 +0,0 @@
etc/csc

@ -1 +0,0 @@
etc/*.cf etc/ops etc/spam etc/csc

@ -1,55 +0,0 @@
#! /bin/sh
### BEGIN INIT INFO
# Provides: ceod
# Required-Start: $remote_fs $syslog $network
# Required-Stop: $remote_fs $syslog $network
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: CEO Daemon
### END INIT INFO
set -e
test -x /usr/sbin/ceod || exit 0
. /lib/lsb/init-functions
case "$1" in
start)
log_daemon_msg "Starting CEO Daemon" "ceod"
if start-stop-daemon --start --quiet --oknodo --pidfile /var/run/ceod.pid --exec /usr/sbin/ceod -- -dq; then
log_end_msg 0
else
log_end_msg 1
fi
;;
stop)
log_daemon_msg "Stopping CEO Daemon" "ceod"
if start-stop-daemon --stop --quiet --oknodo --pidfile /var/run/ceod.pid; then
log_end_msg 0
else
log_end_msg 1
fi
;;
restart|force-reload)
log_daemon_msg "Restarting CEO Daemon" "ceod"
start-stop-daemon --stop --quiet --oknodo --retry 30 --pidfile /var/run/ceod.pid
if start-stop-daemon --start --quiet --oknodo --pidfile /var/run/ceod.pid --exec /usr/sbin/ceod -- -dq; then
log_end_msg 0
else
log_end_msg 1
fi
;;
status)
status_of_proc -p /var/run/ceod.pid /usr/sbin/ceod ceod && exit 0 || exit $?
;;
*)
log_action_msg "Usage: /etc/init.d/ceod {start|stop|force-reload|restart|status}"
exit 1
esac
exit 0

@ -1 +0,0 @@
etc/ldap/schema

@ -1 +0,0 @@
etc/csc.schema etc/ldap/schema

@ -1 +0,0 @@
docs/ceod.8

@ -1 +0,0 @@
docs/ceo.1

739
debian/changelog vendored

@ -1,739 +0,0 @@
ceo (0.7.1-buster1) buster; urgency=medium
* Update mailman path to use virtualenv
-- Max Erenberg <merenber@csclub.uwaterloo.ca> Tue, 18 May 2021 01:45:49 -0400
ceo (0.7.0-buster1) buster; urgency=medium
* Set userPassword field in LDAP for SASL authentication
-- Zachary Seguin <ztseguin@csclub.uwaterloo.ca> Fri, 07 May 2021 21:44:02 -0400
ceo (0.6.0-buster1.2) buster; urgency=medium
* Decrease minimum username length from 3 to 2
-- Max Erenberg <merenber@csclub.uwaterloo.ca> Sun, 02 May 2021 18:02:31 -0400
ceo (0.6.0-buster1.1) buster; urgency=medium
* Use Mailman 3 instead of Mailman 2
-- Max Erenberg <merenber@csclub.uwaterloo.ca> Sun, 11 Apr 2021 21:54:06 -0400
ceo (0.6.0-stretch1) stretch; urgency=high
* Move adduser and mail operations to phosphoric-acid due to decommissioning
of aspartame
* Packaging for stretch
-- Zachary Seguin <ztseguin@csclub.uwaterloo.ca> Sun, 21 Mar 2021 23:04:05 -0400
ceo (0.6.0-buster1) buster; urgency=high
* Move adduser and mail operations to phosphoric-acid due to decommissioning
of aspartame
-- Zachary Seguin <ztseguin@csclub.uwaterloo.ca> Sun, 21 Mar 2021 22:39:05 -0400
ceo (0.5.28-bionic1.1) bionic; urgency=medium
* Packaging for bionic
-- Jennifer Zhou <c7zou@csclub.uwaterloo.ca> Sun, 21 Oct 2018 21:38:57 -0400
ceo (0.5.28-buster1) buster; urgency=medium
* Package for buster
-- Zachary Seguin <ztseguin@csclub.uwaterloo.ca> Sun, 15 Apr 2018 14:31:08 -0400
ceo (0.5.28-xenial1) xenial; urgency=medium
* Build for xenial
-- Zachary Seguin <ztseguin@csclub.uwaterloo.ca> Tue, 02 May 2017 00:24:45 -0400
ceo (0.5.28-jessie1) jessie; urgency=medium
* Build for jessie
-- Zachary Seguin <ztseguin@csclub.uwaterloo.ca> Tue, 02 May 2017 00:16:31 -0400
ceo (0.5.28-stretch1) stretch; urgency=medium
* Check for host (IPv4 or IPV6) or MX record when verying valid email
addresses
-- Zachary Seguin <ztseguin@csclub.uwaterloo.ca> Wed, 01 May 2017 13:07:21 -0500
ceo (0.5.27-stretch1) stretch; urgency=medium
* Build for stretch
-- Zachary Seguin <ztseguin@csclub.uwaterloo.ca> Wed, 11 Jan 2017 16:07:21 -0500
ceo (0.5.27jessie2) jessie; urgency=low
* Include library as a dependency
-- Zachary Seguin <ztseguin@csclub.uwaterloo.ca> Sat, 20 Feb 2016 15:54:29 -0500
ceo (0.5.27trusty2) trusty; urgency=medium
* Include library as a dependency
-- Zachary Seguin <ztseguin@csclub.uwaterloo.ca> Sat, 20 Feb 2016 15:57:18 -0500
ceo (0.5.27trusty1) trusty; urgency=high
* Resolved issue from previous release which resulted in CEO not launching
-- Zachary Seguin <ztseguin@csclub.uwaterloo.ca> Fri, 19 Feb 2016 23:38:41 -0500
ceo (0.5.27jessie1) jessie; urgency=high
* Resolved issue from previous release which resulted in CEO not launching
-- Zachary Seguin <ztseguin@csclub.uwaterloo.ca> Fri, 19 Feb 2016 23:38:41 -0500
ceo (0.5.27jessie) jessie; urgency=medium
* "Library" now launches "librarian"
-- Felix Bauckholt <fbauckho@csclub.uwaterloo.ca> Fri, 19 Feb 2016 22:12:25 -0500
ceo (0.5.26trusty) trusty; urgency=medium
* "Library" now launches "librarian"
-- Felix Bauckholt <fbauckho@csclub.uwaterloo.ca> Fri, 19 Feb 2016 22:07:37 -0500
ceo (0.5.26) jessie; urgency=medium
* Repackage for jessie
* Fix build for latest package versions
-- Zachary Seguin <ztseguin@csclub.uwaterloo.ca> Wed, 11 Nov 2015 22:39:49 -0500
ceo (0.5.25jessie0) jessie; urgency=low
* Replace mention of the safe with the cup.
* Remind users that club accounts are free.
-- Sean Hunt <scshunt@csclub.uwaterloo.ca> Tue, 22 Jul 2014 14:20:16 -0400
ceo (0.5.24ubuntu5) saucy; urgency=low
* Packaging for saucy.
-- Sean Hunt <scshunt@csclub.uwaterloo.ca> Thu, 05 Dec 2013 15:59:17 -0500
ceo (0.5.24jessie0) jessie; urgency=low
* Packaging for jessie.
-- Luqman Aden <laden@csclub.uwaterloo.ca> Thu, 10 Oct 2013 21:51:26 -0400
ceo (0.5.24squeeze0) oldstable; urgency=low
* Rebuild for squeeze, since a wheezy package was accepted there by accident.
-- Jeremy Roman <jbroman@csclub.uwaterloo.ca> Mon, 16 Sep 2013 08:33:58 -0400
ceo (0.5.24) stable; urgency=low
* Fix bug introduced in Kerberos change.
-- Jeremy Roman <jbroman@csclub.uwaterloo.ca> Mon, 16 Sep 2013 08:28:51 -0400
ceo (0.5.23) stable; urgency=low
* Stable is now wheezy; rebuild.
-- Jeremy Roman <jbroman@csclub.uwaterloo.ca> Sat, 07 Sep 2013 11:59:24 -0400
ceo (0.5.22) stable; urgency=low
* Drop support for Kerberos LDAP backend; this is not the current CSC setup.
-- Jeremy Roman <jbroman@csclub.uwaterloo.ca> Sat, 07 Sep 2013 11:45:33 -0400
ceo (0.5.21) testing; urgency=low
* Build with older protoc-c for compatibility with squeeze.
-- Marc Burns <m4burns@csclub.uwaterloo.ca> Tue, 28 May 2013 11:14:36 -0400
ceo (0.5.20) testing; urgency=low
* Work around bug in libgssapi 2.0.25 present in wheezy.
-- Marc Burns <m4burns@csclub.uwaterloo.ca> Tue, 28 May 2013 10:45:09 -0400
ceo (0.5.19ubuntu2) quantal; urgency=low
* Packaging for quantal.
-- Owen Michael Smith <omsmith@gwem.csclub.uwaterloo.ca> Sat, 25 May 2013 19:46:52 -0400
ceo (0.5.19ubuntu1) precise; urgency=low
* Added precise package with changes
-- Sarah Harvey <sharvey@csclub.uwaterloo.ca> Wed, 06 Feb 2013 23:44:18 -0500
ceo (0.5.19) stable; urgency=low
* Updated mail, adduser host to be aspartame, not ginseng (following filesystem migration)
-- Sarah Harvey <sharvey@csclub.uwaterloo.ca> Wed, 06 Feb 2013 23:36:46 -0500
ceo (0.5.18ubuntu1) precise; urgency=low
* Added precise package with changes.
-- Sarah Harvey <sharvey@csclub.uwaterloo.ca> Wed, 12 Sep 2012 08:42:02 -0400
ceo (0.5.18) stable; urgency=low
* Updated mailman host to be mail, not caffeine (following mail container migration)
-- Sarah Harvey <sharvey@csclub.uwaterloo.ca> Mon, 10 Sep 2012 19:06:16 -0400
ceo (0.5.17ubuntu2) precise; urgency=low
* Accidentally merged in broken changes. Fixing.
-- Jeremy Roman <jbroman@csclub.uwaterloo.ca> Thu, 26 Apr 2012 15:19:03 -0400
ceo (0.5.17) stable; urgency=low
* Change behavior of ceod to add Kerberos principal,
* as opposed to changing principal password.
-- Marc Burns <m4burns@csclub.uwaterloo.ca> Fri, 16 Mar 2012 15:27:35 -0400
ceo (0.5.16) stable; urgency=low
* Fix CEO for CMC by allow mailman to be disabled.
-- Michael Spang <mspang@csclub.uwaterloo.ca> Sat, 17 Sep 2011 16:36:01 -0400
ceo (0.5.14) stable; urgency=low
* Add support for sending a welcome message.
-- Jeremy Roman <jbroman@csclub.uwaterloo.ca> Fri, 26 Aug 2011 00:59:08 -0400
ceo (0.5.13) stable; urgency=low
* Fix Mailman path
-- Jeremy Roman <jbroman@csclub.uwaterloo.ca> Mon, 09 May 2011 19:12:09 -0400
ceo (0.5.12) stable; urgency=low
* Change sudoRunAs to sudoRunAsUser.
-- Michael Spang <mspang@csclub.uwaterloo.ca> Sun, 13 Mar 2011 03:24:30 -0400
ceo (0.5.11) stable; urgency=low
* Fix library check in and search bug introduced in 0.5.9+nmu1.
-- Marc Burns <m4burns@csclub.uwaterloo.ca> Fri, 04 Mar 2011 16:52:32 -0500
ceo (0.5.10) stable; urgency=low
* Fix squeeze build warnings
* Add m4burns to debian/control
-- Michael Spang <mspang@csclub.uwaterloo.ca> Fri, 04 Mar 2011 00:47:09 -0500
ceo (0.5.9+nmu1) stable; urgency=low
* Non-maintainer upload.
* Fix library book search page to display message when no books are found.
-- Marc Burns <m4burns@csclub.uwaterloo.ca> Mon, 28 Feb 2011 13:00:24 -0500
ceo (0.5.9) stable; urgency=low
* Fix build for squeeze.
-- Michael Spang <mspang@csclub.uwaterloo.ca> Thu, 14 Oct 2010 14:22:04 -0400
ceo (0.5.8+nmu1) stable; urgency=low
* fixed bug reported by jdonland
-- Jeremy Roman <jbroman@csclub.uwaterloo.ca> Sun, 26 Sep 2010 22:32:50 -0400
ceo (0.5.8) stable; urgency=low
* tab support in most forms (note that the tab key is already bound for the LDAP lookup fields)
* new members can be added for multiple terms without going through renewal
* fix for the squeeze version of urwid
* new members are automatically added to csc-general
-- Jeremy Roman <jbroman@csclub.uwaterloo.ca> Sat, 25 Sep 2010 01:04:02 -0400
ceo (0.5.7+nmu4) stable; urgency=low
* Non-maintainer upload.
* add Office Manager position to positions list
-- Jeremy Roman <jbroman@csclub.uwaterloo.ca> Tue, 14 Sep 2010 18:19:50 -0400
ceo (0.5.7+nmu3) stable; urgency=low
* Added phpmyadmin to mysql info file generated by CEO
-- Michael Ellis <me@michaelellis.ca> Thu, 19 Aug 2010 14:06:16 -0400
ceo (0.5.7+nmu2) stable; urgency=low
* Removed the need for separate entries to manage office and syscom
* Added check to ensure group is valid
-- Michael Ellis <me@michaelellis.ca> Fri, 18 Jun 2010 21:29:48 -0400
ceo (0.5.7+nmu1) stable; urgency=low
* Non-maintainer upload.
* Removed uwdir lookup for expired accounts emailing
-- Michael Ellis <m2ellis@caffeine.csclub.uwaterloo.ca> Tue, 18 May 2010 18:18:02 -0400
ceo (0.5.7) stable; urgency=low
[ Michael Spang ]
* Fix expiredaccounts
[ Michael Ellis ]
* Reworded expired account email. Club rep accounts can be renewed for
free (as usual).
[ Michael Spang ]
* Readd quota support
-- Michael Spang <mspang@csclub.uwaterloo.ca> Sun, 09 May 2010 02:10:48 -0400
ceo (0.5.6) stable; urgency=low
[ Michael Spang ]
* Fix use of freopen
* Fix auth for mysql database creation
[ Jeremy Brandon Roman ]
* added ability to use first letter of menu items
[ Michael Spang ]
* Remove ternary operators
-- Michael Spang <mspang@csclub.uwaterloo.ca> Sun, 20 Dec 2009 13:45:48 -0500
ceo (0.5.5) stable; urgency=low
* Add missing dependency on python-mysql
* Add CLI version of mysql thing
-- Michael Spang <mspang@csclub.uwaterloo.ca> Mon, 02 Nov 2009 20:34:52 +0000
ceo (0.5.4) stable; urgency=low
* Switch from SCTP to TCP
-- Michael Spang <mspang@csclub.uwaterloo.ca> Mon, 02 Nov 2009 03:04:52 +0000
ceo (0.5.3) stable; urgency=low
* Fix gss error reporting bug
* Clarify email forwarding upon renewal
* Fail fast if not authenticated
* Encrypt all post-auth ceoc<->ceod communication
* Improve error handling when writing
-- Michael Spang <mspang@csclub.uwaterloo.ca> Sat, 24 Oct 2009 14:49:51 -0400
ceo (0.5.2) stable; urgency=low
* Clarify search operation in menu
* Move some code
* Fix segfault
* Write mysql file to ~club
* Kill mathsoclist
* Blacklist orphaned/expired from updateprograms
* Add status thing
* Force redraw after status thing
-- Michael Spang <mspang@csclub.uwaterloo.ca> Wed, 16 Sep 2009 18:32:56 -0400
ceo (0.5.1) stable; urgency=low
* Add mysql magic.
* Add email forwarding magic.
* Labels on the menu.
-- Michael Spang <mspang@csclub.uwaterloo.ca> Wed, 09 Sep 2009 17:54:49 -0400
ceo (0.5.0) stable; urgency=low
* Add ceo daemon.
-- Michael Spang <mspang@uwaterloo.ca> Thu, 30 Jul 2009 00:19:42 -0400
ceo (0.4.24) stable; urgency=low
* Bump standards version.
-- Michael Spang <mspang@uwaterloo.ca> Wed, 29 Jul 2009 07:31:24 -0400
ceo (0.4.23) stable; urgency=low
* CEO library now only finds books that are signed out as being overdue.
-- Michael Gregson <mgregson@csclub.uwaterloo.ca> Wed, 11 Mar 2009 03:30:01 -0500
ceo (0.4.22) stable; urgency=low
* CEO now closes window when it should. (Sorry)