Compare commits
13 Commits
4145491373
...
dbe1ef4d09
Author | SHA1 | Date |
---|---|---|
Max Erenberg | dbe1ef4d09 | |
Max Erenberg | d6ce40037d | |
Max Erenberg | f84965c8e1 | |
Max Erenberg | 4394c4e277 | |
Jonathan Leung | b507c56136 | |
Max Erenberg | c0c9736593 | |
Max Erenberg | 1e452d10ce | |
Max Erenberg | 6a1fa81b82 | |
Max Erenberg | 6df1f4d459 | |
Edwin | 2cf9e25b59 | |
Edwin | 9ff3d850c9 | |
Edwin | b4a1373559 | |
Raymond Li | dceb5d6572 |
|
@ -5,7 +5,7 @@ name: default
|
||||||
steps:
|
steps:
|
||||||
# use the step name to mock out the gethostname() call in our tests
|
# use the step name to mock out the gethostname() call in our tests
|
||||||
- name: phosphoric-acid
|
- name: phosphoric-acid
|
||||||
image: python:3.7-buster
|
image: python:3.9-bullseye
|
||||||
# unfortunately we have to do everything in one step because there's no
|
# unfortunately we have to do everything in one step because there's no
|
||||||
# way to share system packages between steps
|
# way to share system packages between steps
|
||||||
commands:
|
commands:
|
||||||
|
@ -25,12 +25,12 @@ steps:
|
||||||
|
|
||||||
services:
|
services:
|
||||||
- name: auth1
|
- name: auth1
|
||||||
image: debian:buster
|
image: debian:bullseye
|
||||||
commands:
|
commands:
|
||||||
- .drone/auth1-setup.sh
|
- .drone/auth1-setup.sh
|
||||||
- sleep infinity
|
- sleep infinity
|
||||||
- name: coffee
|
- name: coffee
|
||||||
image: debian:buster
|
image: debian:bullseye
|
||||||
commands:
|
commands:
|
||||||
- .drone/coffee-setup.sh
|
- .drone/coffee-setup.sh
|
||||||
- sleep infinity
|
- sleep infinity
|
||||||
|
|
|
@ -28,7 +28,6 @@ killall slapd || true
|
||||||
service nslcd stop || true
|
service nslcd stop || true
|
||||||
rm -rf /etc/ldap/slapd.d
|
rm -rf /etc/ldap/slapd.d
|
||||||
rm /var/lib/ldap/*
|
rm /var/lib/ldap/*
|
||||||
cp /usr/share/slapd/DB_CONFIG /var/lib/ldap/DB_CONFIG
|
|
||||||
cp .drone/slapd.conf /etc/ldap/slapd.conf
|
cp .drone/slapd.conf /etc/ldap/slapd.conf
|
||||||
cp .drone/ldap.conf /etc/ldap/ldap.conf
|
cp .drone/ldap.conf /etc/ldap/ldap.conf
|
||||||
cp /usr/share/doc/sudo-ldap/schema.OpenLDAP /etc/ldap/schema/sudo.schema
|
cp /usr/share/doc/sudo-ldap/schema.OpenLDAP /etc/ldap/schema/sudo.schema
|
||||||
|
|
|
@ -11,9 +11,9 @@ add_fqdn_to_hosts $(get_ip_addr auth1) auth1
|
||||||
apt install --no-install-recommends -y default-mysql-server postgresql
|
apt install --no-install-recommends -y default-mysql-server postgresql
|
||||||
|
|
||||||
# MYSQL
|
# MYSQL
|
||||||
service mysql stop
|
service mariadb stop
|
||||||
sed -E -i 's/^(bind-address[[:space:]]+= 127.0.0.1)$/#\1/' /etc/mysql/mariadb.conf.d/50-server.cnf
|
sed -E -i 's/^(bind-address[[:space:]]+= 127\.0\.0\.1)$/#\1/' /etc/mysql/mariadb.conf.d/50-server.cnf
|
||||||
service mysql start
|
service mariadb start
|
||||||
cat <<EOF | mysql
|
cat <<EOF | mysql
|
||||||
CREATE USER IF NOT EXISTS 'mysql' IDENTIFIED BY 'mysql';
|
CREATE USER IF NOT EXISTS 'mysql' IDENTIFIED BY 'mysql';
|
||||||
GRANT ALL PRIVILEGES ON *.* TO 'mysql' WITH GRANT OPTION;
|
GRANT ALL PRIVILEGES ON *.* TO 'mysql' WITH GRANT OPTION;
|
||||||
|
@ -21,7 +21,7 @@ EOF
|
||||||
|
|
||||||
# POSTGRESQL
|
# POSTGRESQL
|
||||||
service postgresql stop
|
service postgresql stop
|
||||||
POSTGRES_DIR=/etc/postgresql/11/main
|
POSTGRES_DIR=/etc/postgresql/*/main
|
||||||
cat <<EOF > $POSTGRES_DIR/pg_hba.conf
|
cat <<EOF > $POSTGRES_DIR/pg_hba.conf
|
||||||
# TYPE DATABASE USER ADDRESS METHOD
|
# TYPE DATABASE USER ADDRESS METHOD
|
||||||
local all postgres peer
|
local all postgres peer
|
||||||
|
|
|
@ -75,6 +75,7 @@ auth_setup() {
|
||||||
# LDAP
|
# LDAP
|
||||||
apt install -y --no-install-recommends libnss-ldapd
|
apt install -y --no-install-recommends libnss-ldapd
|
||||||
service nslcd stop || true
|
service nslcd stop || true
|
||||||
|
mkdir -p /etc/ldap
|
||||||
cp .drone/ldap.conf /etc/ldap/ldap.conf
|
cp .drone/ldap.conf /etc/ldap/ldap.conf
|
||||||
grep -Eq '^map group member uniqueMember$' /etc/nslcd.conf || \
|
grep -Eq '^map group member uniqueMember$' /etc/nslcd.conf || \
|
||||||
echo 'map group member uniqueMember' >> /etc/nslcd.conf
|
echo 'map group member uniqueMember' >> /etc/nslcd.conf
|
||||||
|
|
|
@ -9,6 +9,8 @@ Make sure your GPG key is in /srv/debian/gpg on potassium-benzoate. See
|
||||||
[here](https://wiki.csclub.uwaterloo.ca/Debian_Repository#Step_1:_Add_to_Uploaders)
|
[here](https://wiki.csclub.uwaterloo.ca/Debian_Repository#Step_1:_Add_to_Uploaders)
|
||||||
for instructions.
|
for instructions.
|
||||||
|
|
||||||
|
Make sure you are in the `csc-mirror` group too.
|
||||||
|
|
||||||
## Creating the package
|
## Creating the package
|
||||||
Use Docker/Podman to avoid screwing up your main system.
|
Use Docker/Podman to avoid screwing up your main system.
|
||||||
For example, to create a package for bullseye (replace `podman` with `docker` in all instances below if you're using Docker):
|
For example, to create a package for bullseye (replace `podman` with `docker` in all instances below if you're using Docker):
|
||||||
|
@ -58,7 +60,7 @@ podman cp pyceo-packaging:/home/max/repos/pyceo.tar.gz .
|
||||||
(Replace `/home/max/repos` by the directory in the container with the tarball.)
|
(Replace `/home/max/repos` by the directory in the container with the tarball.)
|
||||||
Now upload the tarball to a CSC machine, e.g.
|
Now upload the tarball to a CSC machine, e.g.
|
||||||
```
|
```
|
||||||
scp pyceo.tar.gz mannitol:~/
|
scp pyceo.tar.gz mannitol:~
|
||||||
```
|
```
|
||||||
SSH into that machine and extract the tarball into a separate directory:
|
SSH into that machine and extract the tarball into a separate directory:
|
||||||
```
|
```
|
||||||
|
|
|
@ -16,7 +16,7 @@ Docker containers instead, which are much easier to work with than the VM.
|
||||||
|
|
||||||
First, make sure you create the virtualenv:
|
First, make sure you create the virtualenv:
|
||||||
```sh
|
```sh
|
||||||
docker run --rm -v "$PWD:$PWD:z" -w "$PWD" python:3.7-buster sh -c 'apt update && apt install -y libaugeas0 && python -m venv venv && . venv/bin/activate && pip install -r requirements.txt -r dev-requirements.txt'
|
docker run --rm -v "$PWD:$PWD:z" -w "$PWD" python:3.9-bullseye sh -c 'apt update && apt install -y libaugeas0 && python -m venv venv && . venv/bin/activate && pip install -r requirements.txt -r dev-requirements.txt'
|
||||||
```
|
```
|
||||||
Then bring up the containers:
|
Then bring up the containers:
|
||||||
```sh
|
```sh
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
1.0.23
|
1.0.24
|
||||||
|
|
|
@ -16,13 +16,22 @@ def positions():
|
||||||
def get():
|
def get():
|
||||||
resp = http_get('/api/positions')
|
resp = http_get('/api/positions')
|
||||||
result = handle_sync_response(resp)
|
result = handle_sync_response(resp)
|
||||||
print_colon_kv(result.items())
|
print_colon_kv([
|
||||||
|
(position, ', '.join(usernames))
|
||||||
|
for position, usernames in result.items()
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
@positions.command(short_help='Update positions')
|
@positions.command(short_help='Update positions')
|
||||||
def set(**kwargs):
|
def set(**kwargs):
|
||||||
body = {k.replace('_', '-'): v for k, v in kwargs.items()}
|
body = {
|
||||||
print_body = {k: v or '' for k, v in body.items()}
|
k.replace('_', '-'): v.replace(' ', '').split(',') if v else None
|
||||||
|
for k, v in kwargs.items()
|
||||||
|
}
|
||||||
|
print_body = {
|
||||||
|
k: ', '.join(v) if v else ''
|
||||||
|
for k, v in body.items()
|
||||||
|
}
|
||||||
click.echo('The positions will be updated:')
|
click.echo('The positions will be updated:')
|
||||||
print_colon_kv(print_body.items())
|
print_colon_kv(print_body.items())
|
||||||
click.confirm('Do you want to continue?', abort=True)
|
click.confirm('Do you want to continue?', abort=True)
|
||||||
|
|
|
@ -19,8 +19,8 @@ class GetPositionsController(Controller):
|
||||||
positions = tui_utils.handle_sync_response(resp, self)
|
positions = tui_utils.handle_sync_response(resp, self)
|
||||||
except Controller.RequestFailed:
|
except Controller.RequestFailed:
|
||||||
return
|
return
|
||||||
for pos, username in positions.items():
|
for pos, usernames in positions.items():
|
||||||
self.model.positions[pos] = username
|
self.model.positions[pos] = ','.join(usernames)
|
||||||
|
|
||||||
def target():
|
def target():
|
||||||
self.view.flash_text.set_text('')
|
self.view.flash_text.set_text('')
|
||||||
|
|
|
@ -17,7 +17,7 @@ class SetPositionsController(Controller):
|
||||||
body = {}
|
body = {}
|
||||||
for pos, field in self.view.position_fields.items():
|
for pos, field in self.view.position_fields.items():
|
||||||
if field.edit_text != '':
|
if field.edit_text != '':
|
||||||
body[pos] = field.edit_text
|
body[pos] = field.edit_text.replace(' ', '').split(',')
|
||||||
model = TransactionModel(
|
model = TransactionModel(
|
||||||
UpdateMemberPositionsTransaction.operations,
|
UpdateMemberPositionsTransaction.operations,
|
||||||
'POST', '/api/positions', json=body
|
'POST', '/api/positions', json=body
|
||||||
|
@ -37,8 +37,8 @@ class SetPositionsController(Controller):
|
||||||
positions = tui_utils.handle_sync_response(resp, self)
|
positions = tui_utils.handle_sync_response(resp, self)
|
||||||
except Controller.RequestFailed:
|
except Controller.RequestFailed:
|
||||||
return
|
return
|
||||||
for pos, username in positions.items():
|
for pos, usernames in positions.items():
|
||||||
self.model.positions[pos] = username
|
self.model.positions[pos] = ','.join(usernames)
|
||||||
|
|
||||||
def target():
|
def target():
|
||||||
self.view.flash_text.set_text('')
|
self.view.flash_text.set_text('')
|
||||||
|
|
|
@ -9,12 +9,4 @@ position_names = {
|
||||||
'imapd': "IMAPD",
|
'imapd': "IMAPD",
|
||||||
'webmaster': "Web Master",
|
'webmaster': "Web Master",
|
||||||
'offsck': "Office Manager",
|
'offsck': "Office Manager",
|
||||||
'ext-affairs-lead': "External Affairs Lead",
|
|
||||||
'marketing-lead': "Marketing Lead",
|
|
||||||
'design-lead': "Design Lead",
|
|
||||||
'events-lead': "Events Lead",
|
|
||||||
'reps-lead': "Reps Lead",
|
|
||||||
'mods-lead': "Mods Lead",
|
|
||||||
'photography-lead': "Photography Lead",
|
|
||||||
'other': "Other",
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,6 +87,9 @@ def space_colon_kv(pairs: List[Tuple[str, str]]) -> List[str]:
|
||||||
maxlen = max(len(key) for key, val in pairs)
|
maxlen = max(len(key) for key, val in pairs)
|
||||||
for key, val in pairs:
|
for key, val in pairs:
|
||||||
if key != '':
|
if key != '':
|
||||||
|
if not val:
|
||||||
|
lines.append(key + ':')
|
||||||
|
continue
|
||||||
prefix = key + ': '
|
prefix = key + ': '
|
||||||
else:
|
else:
|
||||||
# assume this is a continuation from the previous line
|
# assume this is a continuation from the previous line
|
||||||
|
@ -136,6 +139,8 @@ def user_dict_kv(d: Dict) -> List[Tuple[str]]:
|
||||||
pairs.append(('non-member terms', ','.join(_terms)))
|
pairs.append(('non-member terms', ','.join(_terms)))
|
||||||
if 'password' in d:
|
if 'password' in d:
|
||||||
pairs.append(('password', d['password']))
|
pairs.append(('password', d['password']))
|
||||||
|
if 'groups' in d:
|
||||||
|
pairs.append(('groups', ','.join(d['groups'])))
|
||||||
return pairs
|
return pairs
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ from requests_gssapi import HTTPSPNEGOAuth
|
||||||
from zope import component
|
from zope import component
|
||||||
from zope.interface import implementer
|
from zope.interface import implementer
|
||||||
|
|
||||||
from ceo_common.interfaces import IConfig, IHTTPClient
|
from ceo_common.interfaces import IConfig, IHTTPClient, IKerberosService
|
||||||
|
|
||||||
|
|
||||||
@implementer(IHTTPClient)
|
@implementer(IHTTPClient)
|
||||||
|
@ -40,10 +40,18 @@ class HTTPClient:
|
||||||
'opportunistic_auth': True,
|
'opportunistic_auth': True,
|
||||||
'target_name': gssapi.Name('ceod/' + host),
|
'target_name': gssapi.Name('ceod/' + host),
|
||||||
}
|
}
|
||||||
if flask.has_request_context() and 'client_token' in g:
|
if flask.has_request_context():
|
||||||
# This is reached when we are the server and the client has
|
# This is reached when we are the server and the client has
|
||||||
# forwarded their credentials to us.
|
# forwarded their credentials to us.
|
||||||
spnego_kwargs['creds'] = gssapi.Credentials(token=g.client_token)
|
token = None
|
||||||
|
if g.get('need_admin_creds', False):
|
||||||
|
# Some Kerberos bindings in some programming languages can't
|
||||||
|
# perform delegation, so use the admin creds here.
|
||||||
|
token = component.getUtility(IKerberosService).get_admin_creds_token()
|
||||||
|
elif 'client_token' in g:
|
||||||
|
token = g.client_token
|
||||||
|
if token is not None:
|
||||||
|
spnego_kwargs['creds'] = gssapi.Credentials(token=token)
|
||||||
elif delegate:
|
elif delegate:
|
||||||
# This is reached when we are the client and we want to
|
# This is reached when we are the client and we want to
|
||||||
# forward our credentials to the server.
|
# forward our credentials to the server.
|
||||||
|
|
|
@ -71,7 +71,9 @@ def get_user(auth_user: str, username: str):
|
||||||
get_forwarding_addresses = True
|
get_forwarding_addresses = True
|
||||||
ldap_srv = component.getUtility(ILDAPService)
|
ldap_srv = component.getUtility(ILDAPService)
|
||||||
user = ldap_srv.get_user(username)
|
user = ldap_srv.get_user(username)
|
||||||
return user.to_dict(get_forwarding_addresses)
|
user_dict = user.to_dict(get_forwarding_addresses)
|
||||||
|
user_dict['groups'] = ldap_srv.get_groups_for_user(username)
|
||||||
|
return user_dict
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<username>', methods=['PATCH'])
|
@bp.route('/<username>', methods=['PATCH'])
|
||||||
|
|
|
@ -2,6 +2,7 @@ from flask import Blueprint, request
|
||||||
from zope import component
|
from zope import component
|
||||||
|
|
||||||
from .utils import authz_restrict_to_syscom, create_streaming_response
|
from .utils import authz_restrict_to_syscom, create_streaming_response
|
||||||
|
from ceo_common.errors import BadRequest
|
||||||
from ceo_common.interfaces import ILDAPService, IConfig
|
from ceo_common.interfaces import ILDAPService, IConfig
|
||||||
from ceod.transactions.members import UpdateMemberPositionsTransaction
|
from ceod.transactions.members import UpdateMemberPositionsTransaction
|
||||||
|
|
||||||
|
@ -15,10 +16,9 @@ def get_positions():
|
||||||
positions = {}
|
positions = {}
|
||||||
for user in ldap_srv.get_users_with_positions():
|
for user in ldap_srv.get_users_with_positions():
|
||||||
for position in user.positions:
|
for position in user.positions:
|
||||||
if position in positions:
|
if position not in positions:
|
||||||
positions[position] += f", {user.uid}"
|
positions[position] = []
|
||||||
else:
|
positions[position].append(user.uid)
|
||||||
positions[position] = user.uid
|
|
||||||
|
|
||||||
return positions
|
return positions
|
||||||
|
|
||||||
|
@ -34,22 +34,29 @@ def update_positions():
|
||||||
|
|
||||||
# remove falsy values and parse multiple users in each position
|
# remove falsy values and parse multiple users in each position
|
||||||
# Example: "user1,user2, user3" -> ["user1","user2","user3"]
|
# Example: "user1,user2, user3" -> ["user1","user2","user3"]
|
||||||
body = {
|
position_to_usernames = {}
|
||||||
positions: username.replace(' ', '').split(',') for positions, username in body.items()
|
for position, usernames in body.items():
|
||||||
if username
|
if not usernames:
|
||||||
}
|
continue
|
||||||
|
if type(usernames) is list:
|
||||||
|
position_to_usernames[position] = usernames
|
||||||
|
elif type(usernames) is str:
|
||||||
|
position_to_usernames[position] = usernames.replace(' ', '').split(',')
|
||||||
|
else:
|
||||||
|
raise BadRequest('usernames must be a list or comma-separated string')
|
||||||
|
|
||||||
for position in body.keys():
|
# check for duplicates (i.e. one username specified twice in the same list)
|
||||||
|
for usernames in position_to_usernames.values():
|
||||||
|
if len(usernames) != len(set(usernames)):
|
||||||
|
raise BadRequest('username may only be specified at most once for a position')
|
||||||
|
|
||||||
|
for position in position_to_usernames.keys():
|
||||||
if position not in available:
|
if position not in available:
|
||||||
return {
|
raise BadRequest(f'unknown position: {position}')
|
||||||
'error': f'unknown position: {position}'
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
for position in required:
|
for position in required:
|
||||||
if position not in body:
|
if position not in position_to_usernames:
|
||||||
return {
|
raise BadRequest(f'missing required position: {position}')
|
||||||
'error': f'missing required position: {position}'
|
|
||||||
}, 400
|
|
||||||
|
|
||||||
txn = UpdateMemberPositionsTransaction(body)
|
txn = UpdateMemberPositionsTransaction(position_to_usernames)
|
||||||
return create_streaming_response(txn)
|
return create_streaming_response(txn)
|
||||||
|
|
|
@ -103,7 +103,7 @@ class LDAPService:
|
||||||
conn.search(self.ldap_groups_base,
|
conn.search(self.ldap_groups_base,
|
||||||
f'(uniqueMember={self.uid_to_dn(username)})',
|
f'(uniqueMember={self.uid_to_dn(username)})',
|
||||||
attributes=['cn'])
|
attributes=['cn'])
|
||||||
return [entry.cn.value for entry in conn.entries]
|
return sorted([entry.cn.value for entry in conn.entries])
|
||||||
|
|
||||||
def get_display_info_for_users(self, usernames: List[str]) -> List[Dict[str, str]]:
|
def get_display_info_for_users(self, usernames: List[str]) -> List[Dict[str, str]]:
|
||||||
if not usernames:
|
if not usernames:
|
||||||
|
@ -316,12 +316,12 @@ class LDAPService:
|
||||||
self,
|
self,
|
||||||
dry_run: bool = False,
|
dry_run: bool = False,
|
||||||
members: Union[List[str], None] = None,
|
members: Union[List[str], None] = None,
|
||||||
uwldap_batch_size: int = 10,
|
uwldap_batch_size: int = 100,
|
||||||
):
|
):
|
||||||
if members:
|
if members:
|
||||||
filter = '(|' + ''.join([f'(uid={uid})' for uid in members]) + ')'
|
filter = '(|' + ''.join([f'(uid={uid})' for uid in members]) + ')'
|
||||||
else:
|
else:
|
||||||
filter = '(objectClass=*)'
|
filter = '(objectClass=member)'
|
||||||
conn = self._get_ldap_conn()
|
conn = self._get_ldap_conn()
|
||||||
conn.search(
|
conn.search(
|
||||||
self.ldap_users_base, filter, attributes=['uid', 'program'])
|
self.ldap_users_base, filter, attributes=['uid', 'program'])
|
||||||
|
@ -336,12 +336,17 @@ class LDAPService:
|
||||||
batch_uids = uids[i:i + uwldap_batch_size]
|
batch_uids = uids[i:i + uwldap_batch_size]
|
||||||
batch_uw_programs = uwldap_srv.get_programs_for_users(batch_uids)
|
batch_uw_programs = uwldap_srv.get_programs_for_users(batch_uids)
|
||||||
uw_programs.extend(batch_uw_programs)
|
uw_programs.extend(batch_uw_programs)
|
||||||
|
# uw_programs[i] will be None if the 'ou' attribute was not
|
||||||
|
# present in UWLDAP, or if no UWLDAP entry was found at all
|
||||||
|
for i, uw_program in enumerate(uw_programs):
|
||||||
|
if uw_program in (None, 'expired', 'orphaned'):
|
||||||
|
# If the UWLDAP record is orphaned, nonexistent, or missing
|
||||||
|
# data, assume that the member graduated
|
||||||
|
uw_programs[i] = 'Alumni'
|
||||||
users_to_change = [
|
users_to_change = [
|
||||||
(uids[i], csc_programs[i], uw_programs[i])
|
(uids[i], csc_programs[i], uw_programs[i])
|
||||||
for i in range(len(uids))
|
for i in range(len(uids))
|
||||||
if csc_programs[i] != uw_programs[i] and (
|
if csc_programs[i] != uw_programs[i]
|
||||||
uw_programs[i] not in (None, 'expired', 'orphaned')
|
|
||||||
)
|
|
||||||
]
|
]
|
||||||
if dry_run:
|
if dry_run:
|
||||||
return users_to_change
|
return users_to_change
|
||||||
|
|
|
@ -4,7 +4,7 @@ import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import List, Dict, Tuple
|
from typing import List, Dict, Tuple, Union
|
||||||
|
|
||||||
import jinja2
|
import jinja2
|
||||||
from zope import component
|
from zope import component
|
||||||
|
@ -53,6 +53,7 @@ class VHostManager:
|
||||||
self.max_vhosts_per_account = cfg.get('cloud vhosts_max_vhosts_per_account')
|
self.max_vhosts_per_account = cfg.get('cloud vhosts_max_vhosts_per_account')
|
||||||
self.vhost_ip_min = ipaddress.ip_address(cfg.get('cloud vhosts_ip_range_min'))
|
self.vhost_ip_min = ipaddress.ip_address(cfg.get('cloud vhosts_ip_range_min'))
|
||||||
self.vhost_ip_max = ipaddress.ip_address(cfg.get('cloud vhosts_ip_range_max'))
|
self.vhost_ip_max = ipaddress.ip_address(cfg.get('cloud vhosts_ip_range_max'))
|
||||||
|
self.reload_web_server_cmd = cfg.get('cloud vhosts_reload_web_server_cmd')
|
||||||
|
|
||||||
self.acme_challenge_dir = cfg.get('cloud vhosts_acme_challenge_dir')
|
self.acme_challenge_dir = cfg.get('cloud vhosts_acme_challenge_dir')
|
||||||
self.acme_dir = '/root/.acme.sh'
|
self.acme_dir = '/root/.acme.sh'
|
||||||
|
@ -82,12 +83,12 @@ class VHostManager:
|
||||||
"""Return a list of all vhost files for this user."""
|
"""Return a list of all vhost files for this user."""
|
||||||
return glob.glob(os.path.join(self.vhost_dir, username + '_*'))
|
return glob.glob(os.path.join(self.vhost_dir, username + '_*'))
|
||||||
|
|
||||||
def _run(self, args: List[str]):
|
def _run(self, args: Union[List[str], str], **kwargs):
|
||||||
subprocess.run(args, check=True)
|
subprocess.run(args, check=True, **kwargs)
|
||||||
|
|
||||||
def _reload_web_server(self):
|
def _reload_web_server(self):
|
||||||
logger.debug('Reloading NGINX')
|
logger.debug('Reloading NGINX')
|
||||||
self._run(['systemctl', 'reload', 'nginx'])
|
self._run(self.reload_web_server_cmd, shell=True)
|
||||||
|
|
||||||
def is_valid_domain(self, username: str, domain: str) -> bool:
|
def is_valid_domain(self, username: str, domain: str) -> bool:
|
||||||
if VALID_DOMAIN_RE.match(domain) is None:
|
if VALID_DOMAIN_RE.match(domain) is None:
|
||||||
|
@ -150,7 +151,7 @@ class VHostManager:
|
||||||
self.acme_sh, '--install-cert', '-d', domain,
|
self.acme_sh, '--install-cert', '-d', domain,
|
||||||
'--key-file', key_path,
|
'--key-file', key_path,
|
||||||
'--fullchain-file', cert_path,
|
'--fullchain-file', cert_path,
|
||||||
'--reloadcmd', 'systemctl reload nginx',
|
'--reloadcmd', self.reload_web_server_cmd,
|
||||||
])
|
])
|
||||||
|
|
||||||
def _delete_cert(self, domain: str, cert_path: str, key_path: str):
|
def _delete_cert(self, domain: str, cert_path: str, key_path: str):
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import Dict
|
from typing import Dict, List
|
||||||
|
|
||||||
from zope import component
|
from zope import component
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
from ..AbstractTransaction import AbstractTransaction
|
from ..AbstractTransaction import AbstractTransaction
|
||||||
from ceo_common.interfaces import ILDAPService, IConfig, IUser
|
from ceo_common.interfaces import ILDAPService, IConfig, IUser
|
||||||
|
@ -21,21 +20,19 @@ class UpdateMemberPositionsTransaction(AbstractTransaction):
|
||||||
'subscribe_to_mailing_lists',
|
'subscribe_to_mailing_lists',
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, positions_reversed: Dict[str, Union[str, list]]):
|
def __init__(self, position_to_usernames: Dict[str, List[str]]):
|
||||||
# positions_reversed is position -> username
|
# positions_reversed is position -> username
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.ldap_srv = component.getUtility(ILDAPService)
|
self.ldap_srv = component.getUtility(ILDAPService)
|
||||||
|
|
||||||
# Reverse the dict so it's easier to use (username -> positions)
|
# Reverse the dict so it's easier to use (username -> positions)
|
||||||
self.positions = defaultdict(list)
|
self.positions = defaultdict(list)
|
||||||
for position, username in positions_reversed.items():
|
for position, usernames in position_to_usernames.items():
|
||||||
if isinstance(username, str):
|
if isinstance(usernames, list):
|
||||||
self.positions[username].append(position)
|
for username in usernames:
|
||||||
elif isinstance(username, list):
|
self.positions[username].append(position)
|
||||||
for user in username:
|
|
||||||
self.positions[user].append(position)
|
|
||||||
else:
|
else:
|
||||||
raise TypeError("Username(s) under each position must either be a string or a list")
|
raise TypeError("Username(s) under each position must be a list")
|
||||||
|
|
||||||
# a cached Dict of the Users who need to be modified (username -> User)
|
# a cached Dict of the Users who need to be modified (username -> User)
|
||||||
self.users: Dict[str, IUser] = {}
|
self.users: Dict[str, IUser] = {}
|
||||||
|
@ -49,7 +46,7 @@ class UpdateMemberPositionsTransaction(AbstractTransaction):
|
||||||
mailing_lists = cfg.get('auxiliary mailing lists_exec')
|
mailing_lists = cfg.get('auxiliary mailing lists_exec')
|
||||||
|
|
||||||
# position -> username
|
# position -> username
|
||||||
new_positions_reversed = {} # For returning result
|
new_position_to_usernames = {} # For returning result
|
||||||
|
|
||||||
# retrieve User objects and cache them
|
# retrieve User objects and cache them
|
||||||
for username in self.positions:
|
for username in self.positions:
|
||||||
|
@ -71,7 +68,9 @@ class UpdateMemberPositionsTransaction(AbstractTransaction):
|
||||||
|
|
||||||
self.old_positions[username] = old_positions
|
self.old_positions[username] = old_positions
|
||||||
for position in new_positions:
|
for position in new_positions:
|
||||||
new_positions_reversed[position] = username
|
if position not in new_position_to_usernames:
|
||||||
|
new_position_to_usernames[position] = []
|
||||||
|
new_position_to_usernames[position].append(username)
|
||||||
yield 'update_positions_ldap'
|
yield 'update_positions_ldap'
|
||||||
|
|
||||||
# update exec group in LDAP
|
# update exec group in LDAP
|
||||||
|
@ -104,7 +103,7 @@ class UpdateMemberPositionsTransaction(AbstractTransaction):
|
||||||
else:
|
else:
|
||||||
yield 'subscribe_to_mailing_lists'
|
yield 'subscribe_to_mailing_lists'
|
||||||
|
|
||||||
self.finish(new_positions_reversed)
|
self.finish(new_position_to_usernames)
|
||||||
|
|
||||||
def rollback(self):
|
def rollback(self):
|
||||||
if 'update_exec_group_ldap' in self.finished_operations:
|
if 'update_exec_group_ldap' in self.finished_operations:
|
||||||
|
|
|
@ -1,3 +1,16 @@
|
||||||
|
ceo (1.0.24-bullseye1) bullseye; urgency=high
|
||||||
|
|
||||||
|
* Add support for using number in member terms renwewal API
|
||||||
|
* Sort group member listing by WatIAM ID
|
||||||
|
* Add more logging for Cloudstack
|
||||||
|
* Use LDAP instead of NSS
|
||||||
|
* Fix shadowExpire deserialization
|
||||||
|
* Fix email formatting bug in ClubWebHostingService
|
||||||
|
* Check if mail_local_addresses exists in UWLDAP entry
|
||||||
|
* Remove override_dh_systemd_start
|
||||||
|
|
||||||
|
-- Max Erenberg <merenber@csclub.uwaterloo.ca> Sun, 23 Oct 2022 21:41:00 -0400
|
||||||
|
|
||||||
ceo (1.0.23-bullseye1) bullseye; urgency=high
|
ceo (1.0.23-bullseye1) bullseye; urgency=high
|
||||||
|
|
||||||
* Fix some bugs in ClubWebHostingService.
|
* Fix some bugs in ClubWebHostingService.
|
||||||
|
|
|
@ -6,7 +6,8 @@ Standards-Version: 4.3.0
|
||||||
Vcs-Git: https://git.csclub.uwaterloo.ca/public/pyceo.git
|
Vcs-Git: https://git.csclub.uwaterloo.ca/public/pyceo.git
|
||||||
Vcs-Browser: https://git.csclub.uwaterloo.ca/public/pyceo
|
Vcs-Browser: https://git.csclub.uwaterloo.ca/public/pyceo
|
||||||
Uploaders: Max Erenberg <merenber@csclub.uwaterloo.ca>,
|
Uploaders: Max Erenberg <merenber@csclub.uwaterloo.ca>,
|
||||||
Raymond Li <raymo@csclub.uwaterloo.ca>
|
Raymond Li <raymo@csclub.uwaterloo.ca>,
|
||||||
|
Edwin <e42zhang@csclub.uwaterloo.ca>
|
||||||
Build-Depends: debhelper (>= 12.1.1),
|
Build-Depends: debhelper (>= 12.1.1),
|
||||||
python3-dev (>= 3.7),
|
python3-dev (>= 3.7),
|
||||||
python3-venv (>= 3.7),
|
python3-venv (>= 3.7),
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
version: "3.6"
|
version: "3.6"
|
||||||
|
|
||||||
x-common: &common
|
x-common: &common
|
||||||
image: python:3.7-buster
|
image: python:3.9-bullseye
|
||||||
volumes:
|
volumes:
|
||||||
- .:$PWD:z
|
- .:$PWD:z
|
||||||
|
security_opt:
|
||||||
|
- label:disable
|
||||||
environment:
|
environment:
|
||||||
FLASK_APP: ceod.api
|
FLASK_APP: ceod.api
|
||||||
FLASK_ENV: development
|
FLASK_ENV: development
|
||||||
|
@ -14,7 +16,7 @@ x-common: &common
|
||||||
services:
|
services:
|
||||||
auth1:
|
auth1:
|
||||||
<<: *common
|
<<: *common
|
||||||
image: debian:buster
|
image: debian:bullseye
|
||||||
hostname: auth1
|
hostname: auth1
|
||||||
command: auth1
|
command: auth1
|
||||||
|
|
||||||
|
|
|
@ -61,9 +61,9 @@ paths:
|
||||||
program:
|
program:
|
||||||
$ref: "#/components/schemas/Program"
|
$ref: "#/components/schemas/Program"
|
||||||
terms:
|
terms:
|
||||||
$ref: "#/components/schemas/Terms"
|
$ref: "#/components/schemas/TermsOrNumTerms"
|
||||||
non_member_terms:
|
non_member_terms:
|
||||||
$ref: "#/components/schemas/NonMemberTerms"
|
$ref: "#/components/schemas/TermsOrNumTerms"
|
||||||
forwarding_addresses:
|
forwarding_addresses:
|
||||||
$ref: "#/components/schemas/ForwardingAddresses"
|
$ref: "#/components/schemas/ForwardingAddresses"
|
||||||
responses:
|
responses:
|
||||||
|
@ -161,17 +161,11 @@ paths:
|
||||||
- type: object
|
- type: object
|
||||||
properties:
|
properties:
|
||||||
terms:
|
terms:
|
||||||
type: array
|
$ref: "#/components/schemas/TermsOrNumTerms"
|
||||||
description: Terms for which this user will be a member
|
|
||||||
items:
|
|
||||||
$ref: "#/components/schemas/Term"
|
|
||||||
- type: object
|
- type: object
|
||||||
properties:
|
properties:
|
||||||
non_member_terms:
|
non_member_terms:
|
||||||
type: array
|
$ref: "#/components/schemas/TermsOrNumTerms"
|
||||||
description: Terms for which this user will be a club rep
|
|
||||||
items:
|
|
||||||
$ref: "#/components/schemas/Term"
|
|
||||||
example: {"terms": ["f2021"]}
|
example: {"terms": ["f2021"]}
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
|
@ -383,11 +377,14 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
type: string
|
type: array
|
||||||
|
description: list of usernames
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
example:
|
example:
|
||||||
president: user0
|
president: ["user1"]
|
||||||
vice-president: user1
|
vice-president: ["user2", "user3"]
|
||||||
sysadmin: user2
|
sysadmin: ["user4"]
|
||||||
treasurer:
|
treasurer:
|
||||||
post:
|
post:
|
||||||
tags: ['positions']
|
tags: ['positions']
|
||||||
|
@ -404,11 +401,18 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
type: string
|
oneOf:
|
||||||
|
- type: string
|
||||||
|
description: username or comma-separated list of usernames
|
||||||
|
- type: array
|
||||||
|
description: list of usernames
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
example:
|
example:
|
||||||
president: user0
|
president: user1
|
||||||
vice-president: user1
|
vice-president: user2, user3
|
||||||
sysadmin: user2
|
secretary: ["user4", "user5"]
|
||||||
|
sysadmin: ["user6"]
|
||||||
treasurer:
|
treasurer:
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
|
@ -422,7 +426,7 @@ paths:
|
||||||
{"status": "in progress", "operation": "update_positions_ldap"}
|
{"status": "in progress", "operation": "update_positions_ldap"}
|
||||||
{"status": "in progress", "operation": "update_exec_group_ldap"}
|
{"status": "in progress", "operation": "update_exec_group_ldap"}
|
||||||
{"status": "in progress", "operation": "subscribe_to_mailing_list"}
|
{"status": "in progress", "operation": "subscribe_to_mailing_list"}
|
||||||
{"status": "completed", "result": "OK"}
|
{"status": "completed", "result": {"president": ["user1"],"vice-president": ["user2", "user3"],"secretary": ["user4". "user5"],"sysadmin": ["user6"]}}
|
||||||
"400":
|
"400":
|
||||||
description: Failed
|
description: Failed
|
||||||
content:
|
content:
|
||||||
|
@ -883,14 +887,15 @@ components:
|
||||||
example: MAT/Mathematics Computer Science
|
example: MAT/Mathematics Computer Science
|
||||||
Terms:
|
Terms:
|
||||||
type: array
|
type: array
|
||||||
description: Terms for which this user was a member
|
description: List of terms
|
||||||
items:
|
|
||||||
$ref: "#/components/schemas/Term"
|
|
||||||
NonMemberTerms:
|
|
||||||
type: array
|
|
||||||
description: Terms for which this user was a club rep
|
|
||||||
items:
|
items:
|
||||||
$ref: "#/components/schemas/Term"
|
$ref: "#/components/schemas/Term"
|
||||||
|
TermsOrNumTerms:
|
||||||
|
oneOf:
|
||||||
|
- type: integer
|
||||||
|
description: number of additional terms to add
|
||||||
|
example: 1
|
||||||
|
- $ref: "#/components/schemas/Terms"
|
||||||
LoginShell:
|
LoginShell:
|
||||||
type: string
|
type: string
|
||||||
description: Login shell
|
description: Login shell
|
||||||
|
@ -939,9 +944,14 @@ components:
|
||||||
terms:
|
terms:
|
||||||
$ref: "#/components/schemas/Terms"
|
$ref: "#/components/schemas/Terms"
|
||||||
non_member_terms:
|
non_member_terms:
|
||||||
$ref: "#/components/schemas/NonMemberTerms"
|
$ref: "#/components/schemas/Terms"
|
||||||
forwarding_addresses:
|
forwarding_addresses:
|
||||||
$ref: "#/components/schemas/ForwardingAddresses"
|
$ref: "#/components/schemas/ForwardingAddresses"
|
||||||
|
groups:
|
||||||
|
type: array
|
||||||
|
description: Groups for which this user is a member of
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/GroupCN"
|
||||||
UWLDAPUser:
|
UWLDAPUser:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -19,9 +19,7 @@ port = 9987
|
||||||
[positions]
|
[positions]
|
||||||
required = president,vice-president,sysadmin
|
required = president,vice-president,sysadmin
|
||||||
available = president,vice-president,treasurer,secretary,
|
available = president,vice-president,treasurer,secretary,
|
||||||
sysadmin,cro,librarian,imapd,webmaster,offsck,
|
sysadmin,cro,librarian,imapd,webmaster,offsck
|
||||||
ext-affairs-lead,marketing-lead,design-lead,events-lead,
|
|
||||||
reps-lead,mods-lead,photography-lead,other
|
|
||||||
|
|
||||||
[mysql]
|
[mysql]
|
||||||
host = caffeine
|
host = caffeine
|
||||||
|
|
|
@ -64,9 +64,7 @@ exec = exec,exec-moderators
|
||||||
[positions]
|
[positions]
|
||||||
required = president,vice-president,sysadmin
|
required = president,vice-president,sysadmin
|
||||||
available = president,vice-president,treasurer,secretary,
|
available = president,vice-president,treasurer,secretary,
|
||||||
sysadmin,cro,librarian,imapd,webmaster,offsck,
|
sysadmin,cro,librarian,imapd,webmaster,offsck
|
||||||
ext-affairs-lead,marketing-lead,design-lead,events-lead,
|
|
||||||
reps-lead,mods-lead,photography-lead,other
|
|
||||||
|
|
||||||
[mysql]
|
[mysql]
|
||||||
# This is only used on the database_host.
|
# This is only used on the database_host.
|
||||||
|
@ -99,6 +97,7 @@ members_domain = csclub.cloud
|
||||||
k8s_members_domain = k8s.csclub.cloud
|
k8s_members_domain = k8s.csclub.cloud
|
||||||
ip_range_min = 172.19.134.10
|
ip_range_min = 172.19.134.10
|
||||||
ip_range_max = 172.19.134.160
|
ip_range_max = 172.19.134.160
|
||||||
|
reload_web_server_cmd = /root/bin/reload-nginx.sh
|
||||||
|
|
||||||
[k8s]
|
[k8s]
|
||||||
members_clusterrole = csc-members-default
|
members_clusterrole = csc-members-default
|
||||||
|
|
|
@ -6,7 +6,7 @@ gunicorn==20.1.0
|
||||||
Jinja2==3.1.2
|
Jinja2==3.1.2
|
||||||
ldap3==2.9.1
|
ldap3==2.9.1
|
||||||
mysql-connector-python==8.0.26
|
mysql-connector-python==8.0.26
|
||||||
psycopg2==2.9.1
|
psycopg2-binary==2.9.1
|
||||||
python-augeas==1.1.0
|
python-augeas==1.1.0
|
||||||
requests==2.26.0
|
requests==2.26.0
|
||||||
requests-gssapi==1.2.3
|
requests-gssapi==1.2.3
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
import time
|
||||||
|
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
from mysql.connector import connect
|
from mysql.connector import connect
|
||||||
|
@ -10,10 +11,15 @@ from ceo.cli import cli
|
||||||
|
|
||||||
|
|
||||||
def mysql_attempt_connection(host, username, password):
|
def mysql_attempt_connection(host, username, password):
|
||||||
|
# Sometimes, when running the tests locally, I've observed a race condition
|
||||||
|
# where another client can't "see" a database right after we create it.
|
||||||
|
# I only observed this after upgrading the containers to bullseye (MariaDB
|
||||||
|
# version: 10.5.18).
|
||||||
|
time.sleep(0.05)
|
||||||
with connect(
|
with connect(
|
||||||
host=host,
|
host=host,
|
||||||
user=username,
|
user=username,
|
||||||
password=password,
|
password=password,
|
||||||
) as con, con.cursor() as cur:
|
) as con, con.cursor() as cur:
|
||||||
cur.execute("SHOW DATABASES")
|
cur.execute("SHOW DATABASES")
|
||||||
response = cur.fetchall()
|
response = cur.fetchall()
|
||||||
|
@ -34,7 +40,7 @@ def test_mysql(cli_setup, cfg, ldap_user):
|
||||||
|
|
||||||
# create database for user
|
# create database for user
|
||||||
result = runner.invoke(cli, ['mysql', 'create', username], input='y\n')
|
result = runner.invoke(cli, ['mysql', 'create', username], input='y\n')
|
||||||
print(result.output)
|
#print(result.output) # noqa: E265
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert os.path.isfile(info_file_path)
|
assert os.path.isfile(info_file_path)
|
||||||
|
|
||||||
|
|
|
@ -26,8 +26,9 @@ def test_members_get(cli_setup, ldap_user):
|
||||||
f"home directory: {ldap_user.home_directory}\n"
|
f"home directory: {ldap_user.home_directory}\n"
|
||||||
f"is a club: {ldap_user.is_club()}\n"
|
f"is a club: {ldap_user.is_club()}\n"
|
||||||
f"is a club rep: {ldap_user.is_club_rep}\n"
|
f"is a club rep: {ldap_user.is_club_rep}\n"
|
||||||
"forwarding addresses: \n"
|
f"forwarding addresses:\n"
|
||||||
f"member terms: {','.join(ldap_user.terms)}\n"
|
f"member terms: {','.join(ldap_user.terms)}\n"
|
||||||
|
f"groups:\n"
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert result.output == expected
|
assert result.output == expected
|
||||||
|
|
|
@ -44,17 +44,17 @@ vice-president: test_1
|
||||||
sysadmin: test_2
|
sysadmin: test_2
|
||||||
secretary: test_3
|
secretary: test_3
|
||||||
webmaster: test_4
|
webmaster: test_4
|
||||||
treasurer:
|
treasurer:
|
||||||
cro:
|
cro:
|
||||||
librarian:
|
librarian:
|
||||||
imapd:
|
imapd:
|
||||||
offsck:
|
offsck:
|
||||||
Do you want to continue? [y/N]: y
|
Do you want to continue? [y/N]: y
|
||||||
Update positions in LDAP... Done
|
Update positions in LDAP... Done
|
||||||
Update executive group in LDAP... Done
|
Update executive group in LDAP... Done
|
||||||
Subscribe to mailing lists... Done
|
Subscribe to mailing lists... Done
|
||||||
Transaction successfully completed.
|
Transaction successfully completed.
|
||||||
'''[1:] # noqa: W291
|
'''[1:]
|
||||||
|
|
||||||
result = runner.invoke(cli, ['positions', 'get'])
|
result = runner.invoke(cli, ['positions', 'get'])
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
|
@ -71,3 +71,71 @@ webmaster: test_4
|
||||||
for user in users:
|
for user in users:
|
||||||
user.remove_from_ldap()
|
user.remove_from_ldap()
|
||||||
group.remove_from_ldap()
|
group.remove_from_ldap()
|
||||||
|
|
||||||
|
|
||||||
|
def test_positions_multiple_users(cli_setup, g_admin_ctx):
|
||||||
|
runner = CliRunner()
|
||||||
|
|
||||||
|
# Setup test data
|
||||||
|
users = []
|
||||||
|
with g_admin_ctx():
|
||||||
|
for i in range(5):
|
||||||
|
user = User(
|
||||||
|
uid=f'test_{i}',
|
||||||
|
cn=f'Test {i}',
|
||||||
|
given_name='Test',
|
||||||
|
sn=str(i),
|
||||||
|
program='Math',
|
||||||
|
terms=['w2023'],
|
||||||
|
)
|
||||||
|
user.add_to_ldap()
|
||||||
|
users.append(user)
|
||||||
|
group = Group(
|
||||||
|
cn='exec',
|
||||||
|
description='Test Group',
|
||||||
|
gid_number=10500,
|
||||||
|
)
|
||||||
|
group.add_to_ldap()
|
||||||
|
|
||||||
|
result = runner.invoke(cli, [
|
||||||
|
'positions', 'set',
|
||||||
|
'--president', 'test_0',
|
||||||
|
'--vice-president', 'test_1,test_2',
|
||||||
|
'--sysadmin', 'test_2',
|
||||||
|
'--secretary', 'test_3, test_4, test_2',
|
||||||
|
], input='y\n')
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert result.output == '''
|
||||||
|
The positions will be updated:
|
||||||
|
president: test_0
|
||||||
|
vice-president: test_1, test_2
|
||||||
|
sysadmin: test_2
|
||||||
|
secretary: test_3, test_4, test_2
|
||||||
|
treasurer:
|
||||||
|
cro:
|
||||||
|
librarian:
|
||||||
|
imapd:
|
||||||
|
webmaster:
|
||||||
|
offsck:
|
||||||
|
Do you want to continue? [y/N]: y
|
||||||
|
Update positions in LDAP... Done
|
||||||
|
Update executive group in LDAP... Done
|
||||||
|
Subscribe to mailing lists... Done
|
||||||
|
Transaction successfully completed.
|
||||||
|
'''[1:]
|
||||||
|
|
||||||
|
result = runner.invoke(cli, ['positions', 'get'])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert result.output == '''
|
||||||
|
president: test_0
|
||||||
|
secretary: test_2, test_3, test_4
|
||||||
|
sysadmin: test_2
|
||||||
|
vice-president: test_1, test_2
|
||||||
|
'''[1:]
|
||||||
|
|
||||||
|
# Cleanup test data
|
||||||
|
with g_admin_ctx():
|
||||||
|
for user in users:
|
||||||
|
user.remove_from_ldap()
|
||||||
|
group.remove_from_ldap()
|
||||||
|
|
|
@ -15,9 +15,7 @@ port = 9987
|
||||||
[positions]
|
[positions]
|
||||||
required = president,vice-president,sysadmin
|
required = president,vice-president,sysadmin
|
||||||
available = president,vice-president,treasurer,secretary,
|
available = president,vice-president,treasurer,secretary,
|
||||||
sysadmin,cro,librarian,imapd,webmaster,offsck,
|
sysadmin,cro,librarian,imapd,webmaster,offsck
|
||||||
ext-affairs-lead,marketing-lead,design-lead,events-lead,
|
|
||||||
reps-lead,mods-lead,photography-lead,other
|
|
||||||
|
|
||||||
[mysql]
|
[mysql]
|
||||||
host = coffee
|
host = coffee
|
||||||
|
|
|
@ -127,6 +127,7 @@ def test_api_get_user(cfg, client, create_user_result):
|
||||||
del old_data['password']
|
del old_data['password']
|
||||||
|
|
||||||
status, data = client.get(f'/api/members/{uid}')
|
status, data = client.get(f'/api/members/{uid}')
|
||||||
|
del data['groups']
|
||||||
assert status == 200
|
assert status == 200
|
||||||
assert data == old_data
|
assert data == old_data
|
||||||
|
|
||||||
|
@ -262,6 +263,7 @@ def test_authz_check(client, create_user_result):
|
||||||
del old_data['password']
|
del old_data['password']
|
||||||
del old_data['forwarding_addresses']
|
del old_data['forwarding_addresses']
|
||||||
_, data = client.get(f'/api/members/{uid}', principal='regular1')
|
_, data = client.get(f'/api/members/{uid}', principal='regular1')
|
||||||
|
del data['groups']
|
||||||
assert data == old_data
|
assert data == old_data
|
||||||
|
|
||||||
# If we're syscom but we don't pass credentials, the request should fail
|
# If we're syscom but we don't pass credentials, the request should fail
|
||||||
|
|
|
@ -7,8 +7,8 @@ def test_get_positions(client, ldap_user, g_admin_ctx):
|
||||||
status, data = client.get('/api/positions')
|
status, data = client.get('/api/positions')
|
||||||
assert status == 200
|
assert status == 200
|
||||||
expected = {
|
expected = {
|
||||||
'president': ldap_user.uid,
|
'president': [ldap_user.uid],
|
||||||
'treasurer': ldap_user.uid,
|
'treasurer': [ldap_user.uid],
|
||||||
}
|
}
|
||||||
assert data == expected
|
assert data == expected
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ def test_set_positions(cfg, client, g_admin_ctx, mock_mailman_server):
|
||||||
|
|
||||||
users = []
|
users = []
|
||||||
with g_admin_ctx():
|
with g_admin_ctx():
|
||||||
for uid in ['test_1', 'test_2', 'test_3', 'test_4']:
|
for uid in ['test1', 'test2', 'test3', 'test4']:
|
||||||
user = User(uid=uid, cn='Some Name', given_name='Some', sn='Name',
|
user = User(uid=uid, cn='Some Name', given_name='Some', sn='Name',
|
||||||
terms=['s2021'])
|
terms=['s2021'])
|
||||||
user.add_to_ldap()
|
user.add_to_ldap()
|
||||||
|
@ -31,23 +31,23 @@ def test_set_positions(cfg, client, g_admin_ctx, mock_mailman_server):
|
||||||
try:
|
try:
|
||||||
# missing required position
|
# missing required position
|
||||||
status, _ = client.post('/api/positions', json={
|
status, _ = client.post('/api/positions', json={
|
||||||
'vice-president': 'test_1',
|
'vice-president': 'test1',
|
||||||
})
|
})
|
||||||
assert status == 400
|
assert status == 400
|
||||||
|
|
||||||
# non-existent position
|
# non-existent position
|
||||||
status, _ = client.post('/api/positions', json={
|
status, _ = client.post('/api/positions', json={
|
||||||
'president': 'test_1',
|
'president': 'test1',
|
||||||
'vice-president': 'test_2',
|
'vice-president': 'test2',
|
||||||
'sysadmin': 'test_3',
|
'sysadmin': 'test3',
|
||||||
'no-such-position': 'test_3',
|
'no-such-position': 'test3',
|
||||||
})
|
})
|
||||||
assert status == 400
|
assert status == 400
|
||||||
|
|
||||||
status, data = client.post('/api/positions', json={
|
status, data = client.post('/api/positions', json={
|
||||||
'president': 'test_1',
|
'president': 'test1',
|
||||||
'vice-president': 'test_2',
|
'vice-president': 'test2',
|
||||||
'sysadmin': 'test_3',
|
'sysadmin': 'test3',
|
||||||
})
|
})
|
||||||
assert status == 200
|
assert status == 200
|
||||||
expected = [
|
expected = [
|
||||||
|
@ -55,16 +55,16 @@ def test_set_positions(cfg, client, g_admin_ctx, mock_mailman_server):
|
||||||
{"status": "in progress", "operation": "update_exec_group_ldap"},
|
{"status": "in progress", "operation": "update_exec_group_ldap"},
|
||||||
{"status": "in progress", "operation": "subscribe_to_mailing_lists"},
|
{"status": "in progress", "operation": "subscribe_to_mailing_lists"},
|
||||||
{"status": "completed", "result": {
|
{"status": "completed", "result": {
|
||||||
"president": "test_1",
|
"president": ["test1"],
|
||||||
"vice-president": "test_2",
|
"vice-president": ["test2"],
|
||||||
"sysadmin": "test_3",
|
"sysadmin": ["test3"],
|
||||||
}},
|
}},
|
||||||
]
|
]
|
||||||
assert data == expected
|
assert data == expected
|
||||||
# make sure execs were added to exec group
|
# make sure execs were added to exec group
|
||||||
status, data = client.get('/api/groups/exec')
|
status, data = client.get('/api/groups/exec')
|
||||||
assert status == 200
|
assert status == 200
|
||||||
expected = ['test_1', 'test_2', 'test_3']
|
expected = ['test1', 'test2', 'test3']
|
||||||
assert sorted([item['uid'] for item in data['members']]) == expected
|
assert sorted([item['uid'] for item in data['members']]) == expected
|
||||||
# make sure execs were subscribed to mailing lists
|
# make sure execs were subscribed to mailing lists
|
||||||
addresses = [f'{uid}@{base_domain}' for uid in expected]
|
addresses = [f'{uid}@{base_domain}' for uid in expected]
|
||||||
|
@ -72,20 +72,33 @@ def test_set_positions(cfg, client, g_admin_ctx, mock_mailman_server):
|
||||||
assert sorted(mock_mailman_server.subscriptions[mailing_list]) == addresses
|
assert sorted(mock_mailman_server.subscriptions[mailing_list]) == addresses
|
||||||
|
|
||||||
_, data = client.post('/api/positions', json={
|
_, data = client.post('/api/positions', json={
|
||||||
'president': 'test_1',
|
'president': 'test1',
|
||||||
'vice-president': 'test_2',
|
'vice-president': 'test2',
|
||||||
'sysadmin': 'test_2',
|
'sysadmin': 'test2',
|
||||||
'treasurer': 'test_4',
|
'treasurer': 'test4',
|
||||||
})
|
})
|
||||||
assert data[-1]['status'] == 'completed'
|
assert data[-1]['status'] == 'completed'
|
||||||
# make sure old exec was removed from group
|
# make sure old exec was removed from group
|
||||||
expected = ['test_1', 'test_2', 'test_4']
|
expected = ['test1', 'test2', 'test4']
|
||||||
_, data = client.get('/api/groups/exec')
|
_, data = client.get('/api/groups/exec')
|
||||||
assert sorted([item['uid'] for item in data['members']]) == expected
|
assert sorted([item['uid'] for item in data['members']]) == expected
|
||||||
# make sure old exec was removed from mailing lists
|
# make sure old exec was removed from mailing lists
|
||||||
addresses = [f'{uid}@{base_domain}' for uid in expected]
|
addresses = [f'{uid}@{base_domain}' for uid in expected]
|
||||||
for mailing_list in mailing_lists:
|
for mailing_list in mailing_lists:
|
||||||
assert sorted(mock_mailman_server.subscriptions[mailing_list]) == addresses
|
assert sorted(mock_mailman_server.subscriptions[mailing_list]) == addresses
|
||||||
|
|
||||||
|
# multiple users per position
|
||||||
|
status, data = client.post('/api/positions', json={
|
||||||
|
'president': 'test1',
|
||||||
|
'vice-president': ['test2', 'test3'],
|
||||||
|
'sysadmin': 'test2, test3,test4',
|
||||||
|
})
|
||||||
|
assert status == 200
|
||||||
|
assert data[-1] == {'status': 'completed', 'result': {
|
||||||
|
'president': ['test1'],
|
||||||
|
'vice-president': ['test2', 'test3'],
|
||||||
|
'sysadmin': ['test2', 'test3', 'test4'],
|
||||||
|
}}
|
||||||
finally:
|
finally:
|
||||||
with g_admin_ctx():
|
with g_admin_ctx():
|
||||||
for user in users:
|
for user in users:
|
||||||
|
|
|
@ -60,9 +60,7 @@ exec = exec
|
||||||
[positions]
|
[positions]
|
||||||
required = president,vice-president,sysadmin
|
required = president,vice-president,sysadmin
|
||||||
available = president,vice-president,treasurer,secretary,
|
available = president,vice-president,treasurer,secretary,
|
||||||
sysadmin,cro,librarian,imapd,webmaster,offsck,
|
sysadmin,cro,librarian,imapd,webmaster,offsck
|
||||||
ext-affairs-lead,marketing-lead,design-lead,events-lead,
|
|
||||||
reps-lead,mods-lead,photography-lead,other
|
|
||||||
|
|
||||||
[mysql]
|
[mysql]
|
||||||
username = mysql
|
username = mysql
|
||||||
|
@ -93,6 +91,7 @@ members_domain = csclub.cloud
|
||||||
k8s_members_domain = k8s.csclub.cloud
|
k8s_members_domain = k8s.csclub.cloud
|
||||||
ip_range_min = 172.19.134.10
|
ip_range_min = 172.19.134.10
|
||||||
ip_range_max = 172.19.134.160
|
ip_range_max = 172.19.134.160
|
||||||
|
reload_web_server_cmd = systemctl reload nginx
|
||||||
|
|
||||||
[k8s]
|
[k8s]
|
||||||
members_clusterrole = csc-members-default
|
members_clusterrole = csc-members-default
|
||||||
|
|
|
@ -59,9 +59,7 @@ exec = exec
|
||||||
[positions]
|
[positions]
|
||||||
required = president,vice-president,sysadmin
|
required = president,vice-president,sysadmin
|
||||||
available = president,vice-president,treasurer,secretary,
|
available = president,vice-president,treasurer,secretary,
|
||||||
sysadmin,cro,librarian,imapd,webmaster,offsck,
|
sysadmin,cro,librarian,imapd,webmaster,offsck
|
||||||
ext-affairs-lead,marketing-lead,design-lead,events-lead,
|
|
||||||
reps-lead,mods-lead,photography-lead,other
|
|
||||||
|
|
||||||
[mysql]
|
[mysql]
|
||||||
username = mysql
|
username = mysql
|
||||||
|
@ -92,6 +90,7 @@ members_domain = csclub.cloud
|
||||||
k8s_members_domain = k8s.csclub.cloud
|
k8s_members_domain = k8s.csclub.cloud
|
||||||
ip_range_min = 172.19.134.10
|
ip_range_min = 172.19.134.10
|
||||||
ip_range_max = 172.19.134.160
|
ip_range_max = 172.19.134.160
|
||||||
|
reload_web_server_cmd = systemctl reload nginx
|
||||||
|
|
||||||
[k8s]
|
[k8s]
|
||||||
members_clusterrole = csc-members-default
|
members_clusterrole = csc-members-default
|
||||||
|
|
|
@ -298,6 +298,17 @@ def uwldap_srv(cfg, ldap_conn):
|
||||||
delete_subtree(conn, base_dn)
|
delete_subtree(conn, base_dn)
|
||||||
|
|
||||||
conn.add(base_dn, 'organizationalUnit')
|
conn.add(base_dn, 'organizationalUnit')
|
||||||
|
conn.add(
|
||||||
|
f'uid=ctdalek,{base_dn}',
|
||||||
|
['inetLocalMailRecipient', 'inetOrgPerson', 'organizationalPerson', 'person'],
|
||||||
|
{
|
||||||
|
'mailLocalAddress': 'ctdalek@uwaterloo.internal',
|
||||||
|
'ou': 'Math',
|
||||||
|
'cn': 'Calum T. Dalek',
|
||||||
|
'sn': 'Dalek',
|
||||||
|
'givenName': 'Calum',
|
||||||
|
},
|
||||||
|
)
|
||||||
_uwldap_srv = UWLDAPService()
|
_uwldap_srv = UWLDAPService()
|
||||||
component.getGlobalSiteManager().registerUtility(_uwldap_srv, IUWLDAPService)
|
component.getGlobalSiteManager().registerUtility(_uwldap_srv, IUWLDAPService)
|
||||||
yield _uwldap_srv
|
yield _uwldap_srv
|
||||||
|
|
Loading…
Reference in New Issue