Add linting pre-commit hook and hook install script #86
|
@ -5,7 +5,7 @@ name: default
|
|||
steps:
|
||||
# use the step name to mock out the gethostname() call in our tests
|
||||
- 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
|
||||
# way to share system packages between steps
|
||||
commands:
|
||||
|
@ -25,12 +25,12 @@ steps:
|
|||
|
||||
services:
|
||||
- name: auth1
|
||||
image: debian:buster
|
||||
image: debian:bullseye
|
||||
commands:
|
||||
- .drone/auth1-setup.sh
|
||||
- sleep infinity
|
||||
- name: coffee
|
||||
image: debian:buster
|
||||
image: debian:bullseye
|
||||
commands:
|
||||
- .drone/coffee-setup.sh
|
||||
- sleep infinity
|
||||
|
|
|
@ -28,7 +28,6 @@ killall slapd || true
|
|||
service nslcd stop || true
|
||||
rm -rf /etc/ldap/slapd.d
|
||||
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/ldap.conf /etc/ldap/ldap.conf
|
||||
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
|
||||
|
||||
# MYSQL
|
||||
service mysql stop
|
||||
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 stop
|
||||
sed -E -i 's/^(bind-address[[:space:]]+= 127\.0\.0\.1)$/#\1/' /etc/mysql/mariadb.conf.d/50-server.cnf
|
||||
service mariadb start
|
||||
cat <<EOF | mysql
|
||||
CREATE USER IF NOT EXISTS 'mysql' IDENTIFIED BY 'mysql';
|
||||
GRANT ALL PRIVILEGES ON *.* TO 'mysql' WITH GRANT OPTION;
|
||||
|
@ -21,7 +21,7 @@ EOF
|
|||
|
||||
# POSTGRESQL
|
||||
service postgresql stop
|
||||
POSTGRES_DIR=/etc/postgresql/11/main
|
||||
POSTGRES_DIR=/etc/postgresql/*/main
|
||||
cat <<EOF > $POSTGRES_DIR/pg_hba.conf
|
||||
# TYPE DATABASE USER ADDRESS METHOD
|
||||
local all postgres peer
|
||||
|
|
|
@ -75,6 +75,7 @@ auth_setup() {
|
|||
# LDAP
|
||||
apt install -y --no-install-recommends libnss-ldapd
|
||||
service nslcd stop || true
|
||||
mkdir -p /etc/ldap
|
||||
cp .drone/ldap.conf /etc/ldap/ldap.conf
|
||||
grep -Eq '^map group member uniqueMember$' /etc/nslcd.conf || \
|
||||
echo 'map group member uniqueMember' >> /etc/nslcd.conf
|
||||
|
|
|
@ -1 +1 @@
|
|||
1.0.24
|
||||
1.0.26
|
||||
|
|
|
@ -28,6 +28,11 @@ def activate():
|
|||
'Congratulations! Your cloud account has been activated.',
|
||||
f'You may now login into https://cloud.{base_domain} with your CSC credentials.',
|
||||
"Make sure to enter 'Members' for the domain (no quotes).",
|
||||
'',
|
||||
'Please note that your cloud account will be PERMANENTLY DELETED when',
|
||||
'your CSC membership expires, so make sure to purchase enough membership',
|
||||
'terms in advance. You will receive a warning email one week before your',
|
||||
'cloud account is deleted, so please make sure to check your Junk folder.',
|
||||
]
|
||||
for line in lines:
|
||||
click.echo(line)
|
||||
|
|
|
@ -149,3 +149,15 @@ def delete(group_name):
|
|||
click.confirm(f"Are you sure you want to delete {group_name}?", abort=True)
|
||||
resp = http_delete(f'/api/groups/{group_name}')
|
||||
handle_stream_response(resp, DeleteGroupTransaction.operations)
|
||||
|
||||
|
||||
@groups.command(short_help='Search for groups')
|
||||
@click.argument('query')
|
||||
@click.option('--count', default=10, help='number of results to show')
|
||||
def search(query, count):
|
||||
check_if_in_development()
|
||||
resp = http_get(f'/api/groups/search/{query}/{count}')
|
||||
result = handle_sync_response(resp)
|
||||
for cn in result:
|
||||
if cn != "":
|
||||
click.echo(cn)
|
||||
|
|
|
@ -16,13 +16,22 @@ def positions():
|
|||
def get():
|
||||
resp = http_get('/api/positions')
|
||||
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')
|
||||
def set(**kwargs):
|
||||
body = {k.replace('_', '-'): v for k, v in kwargs.items()}
|
||||
print_body = {k: v or '' for k, v in body.items()}
|
||||
body = {
|
||||
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:')
|
||||
print_colon_kv(print_body.items())
|
||||
click.confirm('Do you want to continue?', abort=True)
|
||||
|
|
|
@ -19,8 +19,8 @@ class GetPositionsController(Controller):
|
|||
positions = tui_utils.handle_sync_response(resp, self)
|
||||
except Controller.RequestFailed:
|
||||
return
|
||||
for pos, username in positions.items():
|
||||
self.model.positions[pos] = username
|
||||
for pos, usernames in positions.items():
|
||||
self.model.positions[pos] = ','.join(usernames)
|
||||
|
||||
def target():
|
||||
self.view.flash_text.set_text('')
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
from ceo.utils import http_get
|
||||
from .Controller import Controller
|
||||
from .SyncRequestController import SyncRequestController
|
||||
from ceo.tui.views import SearchGroupResponseView, GetGroupResponseView
|
||||
|
||||
|
||||
# this is a little bit bad because it relies on zero coupling between
|
||||
# the GetGroupResponseView and the GetGroupController
|
||||
# coupling is also introduced between this controller and the
|
||||
# SearchGroupResponseView as it requires this class's callback
|
||||
class SearchGroupController(SyncRequestController):
|
||||
def __init__(self, model, app):
|
||||
super().__init__(model, app)
|
||||
|
||||
def get_resp(self):
|
||||
if self.model.want_info:
|
||||
return http_get(f'/api/groups/{self.model.name}')
|
||||
else:
|
||||
return http_get(f'/api/groups/search/{self.model.name}/{self.model.count}')
|
||||
|
||||
def get_response_view(self):
|
||||
if self.model.want_info:
|
||||
return GetGroupResponseView(self.model, self, self.app)
|
||||
else:
|
||||
return SearchGroupResponseView(self.model, self, self.app)
|
||||
|
||||
def group_info_callback(self, button, cn):
|
||||
self.model.name = cn
|
||||
self.model.want_info = True
|
||||
self.request_in_progress = False
|
||||
self.on_next_button_pressed(button)
|
||||
|
||||
def on_next_button_pressed(self, button):
|
||||
try:
|
||||
if not self.model.want_info:
|
||||
self.model.name = self.get_username_from_view()
|
||||
self.model.count = 10
|
||||
except Controller.InvalidInput:
|
||||
return
|
||||
self.on_confirmation_button_pressed(button)
|
|
@ -17,7 +17,7 @@ class SetPositionsController(Controller):
|
|||
body = {}
|
||||
for pos, field in self.view.position_fields.items():
|
||||
if field.edit_text != '':
|
||||
body[pos] = field.edit_text
|
||||
body[pos] = field.edit_text.replace(' ', '').split(',')
|
||||
model = TransactionModel(
|
||||
UpdateMemberPositionsTransaction.operations,
|
||||
'POST', '/api/positions', json=body
|
||||
|
@ -37,8 +37,8 @@ class SetPositionsController(Controller):
|
|||
positions = tui_utils.handle_sync_response(resp, self)
|
||||
except Controller.RequestFailed:
|
||||
return
|
||||
for pos, username in positions.items():
|
||||
self.model.positions[pos] = username
|
||||
for pos, usernames in positions.items():
|
||||
self.model.positions[pos] = ','.join(usernames)
|
||||
|
||||
def target():
|
||||
self.view.flash_text.set_text('')
|
||||
|
|
|
@ -8,6 +8,7 @@ from .ResetPasswordController import ResetPasswordController
|
|||
from .ChangeLoginShellController import ChangeLoginShellController
|
||||
from .AddGroupController import AddGroupController
|
||||
from .GetGroupController import GetGroupController
|
||||
from .SearchGroupController import SearchGroupController
|
||||
from .AddMemberToGroupController import AddMemberToGroupController
|
||||
from .RemoveMemberFromGroupController import RemoveMemberFromGroupController
|
||||
from .CreateDatabaseController import CreateDatabaseController
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
class SearchGroupModel:
|
||||
name = 'SearchGroup'
|
||||
title = 'Search groups'
|
||||
|
||||
def __init__(self):
|
||||
self.name = ''
|
||||
self.resp_json = None
|
||||
self.count = 10
|
||||
self.want_info = False
|
|
@ -5,6 +5,7 @@ from .ResetPasswordModel import ResetPasswordModel
|
|||
from .ChangeLoginShellModel import ChangeLoginShellModel
|
||||
from .AddGroupModel import AddGroupModel
|
||||
from .GetGroupModel import GetGroupModel
|
||||
from .SearchGroupModel import SearchGroupModel
|
||||
from .AddMemberToGroupModel import AddMemberToGroupModel
|
||||
from .RemoveMemberFromGroupModel import RemoveMemberFromGroupModel
|
||||
from .CreateDatabaseModel import CreateDatabaseModel
|
||||
|
@ -29,6 +30,7 @@ class WelcomeModel:
|
|||
'Groups': [
|
||||
AddGroupModel,
|
||||
GetGroupModel,
|
||||
SearchGroupModel,
|
||||
AddMemberToGroupModel,
|
||||
RemoveMemberFromGroupModel,
|
||||
],
|
||||
|
|
|
@ -6,6 +6,7 @@ from .ResetPasswordModel import ResetPasswordModel
|
|||
from .ChangeLoginShellModel import ChangeLoginShellModel
|
||||
from .AddGroupModel import AddGroupModel
|
||||
from .GetGroupModel import GetGroupModel
|
||||
from .SearchGroupModel import SearchGroupModel
|
||||
from .AddMemberToGroupModel import AddMemberToGroupModel
|
||||
from .RemoveMemberFromGroupModel import RemoveMemberFromGroupModel
|
||||
from .CreateDatabaseModel import CreateDatabaseModel
|
||||
|
|
|
@ -25,6 +25,7 @@ def handle_sync_response(resp, controller):
|
|||
raise Controller.RequestFailed()
|
||||
|
||||
|
||||
# this can probably be simplified with getattr or something
|
||||
def get_mvc(app, name):
|
||||
if name == WelcomeModel.name:
|
||||
model = WelcomeModel()
|
||||
|
@ -58,6 +59,10 @@ def get_mvc(app, name):
|
|||
model = GetGroupModel()
|
||||
controller = GetGroupController(model, app)
|
||||
view = GetGroupView(model, controller, app)
|
||||
elif name == SearchGroupModel.name:
|
||||
model = SearchGroupModel()
|
||||
controller = SearchGroupController(model, app)
|
||||
view = SearchGroupView(model, controller, app)
|
||||
elif name == AddMemberToGroupModel.name:
|
||||
model = AddMemberToGroupModel()
|
||||
controller = AddMemberToGroupController(model, app)
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import urwid
|
||||
|
||||
from .ColumnResponseView import ColumnResponseView
|
||||
from .utils import decorate_button
|
||||
|
||||
|
||||
class SearchGroupResponseView(ColumnResponseView):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
matches = self.model.resp_json.copy()
|
||||
|
||||
rows = [(urwid.Text(resp),
|
||||
decorate_button(urwid.Button('more info', on_press=self.create_callback(resp))))
|
||||
for resp in matches if resp != '']
|
||||
|
||||
self.set_rows(rows, on_next=self.controller.get_next_menu_callback('Welcome'))
|
||||
|
||||
def create_callback(self, cn):
|
||||
def callback(button):
|
||||
self.controller.group_info_callback(button, cn)
|
||||
return callback
|
|
@ -0,0 +1,17 @@
|
|||
import urwid
|
||||
|
||||
from .ColumnView import ColumnView
|
||||
|
||||
|
||||
class SearchGroupView(ColumnView):
|
||||
def __init__(self, model, controller, app):
|
||||
super().__init__(model, controller, app)
|
||||
# used for query, consider combining Controller.user_name_from_view and Controller.get_group_name_from_view
|
||||
self.username_edit = urwid.Edit()
|
||||
rows = [
|
||||
(
|
||||
urwid.Text('Query:', align='right'),
|
||||
self.username_edit
|
||||
)
|
||||
]
|
||||
self.set_rows(rows)
|
|
@ -16,6 +16,8 @@ from .AddGroupView import AddGroupView
|
|||
from .AddGroupConfirmationView import AddGroupConfirmationView
|
||||
from .GetGroupView import GetGroupView
|
||||
from .GetGroupResponseView import GetGroupResponseView
|
||||
from .SearchGroupView import SearchGroupView
|
||||
from .SearchGroupResponseView import SearchGroupResponseView
|
||||
from .AddMemberToGroupView import AddMemberToGroupView
|
||||
from .AddMemberToGroupConfirmationView import AddMemberToGroupConfirmationView
|
||||
from .RemoveMemberFromGroupView import RemoveMemberFromGroupView
|
||||
|
|
|
@ -87,6 +87,9 @@ def space_colon_kv(pairs: List[Tuple[str, str]]) -> List[str]:
|
|||
maxlen = max(len(key) for key, val in pairs)
|
||||
for key, val in pairs:
|
||||
if key != '':
|
||||
if not val:
|
||||
lines.append(key + ':')
|
||||
continue
|
||||
prefix = key + ': '
|
||||
else:
|
||||
# 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)))
|
||||
if 'password' in d:
|
||||
pairs.append(('password', d['password']))
|
||||
if 'groups' in d:
|
||||
pairs.append(('groups', ','.join(d['groups'])))
|
||||
return pairs
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,52 @@
|
|||
import datetime
|
||||
|
||||
|
||||
class fuzzy_result:
|
||||
def __init__(self, string, score):
|
||||
self.string = string
|
||||
self.score = score
|
||||
|
||||
# consider a score worse if the edit distance is larger
|
||||
def __lt__(self, other):
|
||||
return self.score > other.score
|
||||
|
||||
def __gt__(self, other):
|
||||
return self.score < other.score
|
||||
|
||||
def __le__(self, other):
|
||||
return self.score >= other.score
|
||||
|
||||
def __ge__(self, other):
|
||||
return self.score <= other.score
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.score == other.score
|
||||
|
||||
def __ne__(self, other):
|
||||
return self.score != other.score
|
||||
|
||||
|
||||
# compute levenshtein edit distance, adapted from rosetta code
|
||||
def fuzzy_match(s1, s2):
|
||||
if len(s1) == 0:
|
||||
return len(s2)
|
||||
if len(s2) == 0:
|
||||
return len(s1)
|
||||
edits = [i for i in range(len(s2) + 1)]
|
||||
for i in range(len(s1)):
|
||||
corner = i
|
||||
edits[0] = i + 1
|
||||
for j in range(len(s2)):
|
||||
upper = edits[j + 1]
|
||||
if s1[i] == s2[j]:
|
||||
edits[j + 1] = corner
|
||||
else:
|
||||
m = min(corner, upper, edits[j])
|
||||
edits[j + 1] = m + 1
|
||||
corner = upper
|
||||
return edits[-1]
|
||||
|
||||
|
||||
def get_current_datetime() -> datetime.datetime:
|
||||
# We place this in a separate function so that we can mock it out
|
||||
# in our unit tests.
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
from flask import Blueprint, request
|
||||
from flask.json import jsonify
|
||||
from zope import component
|
||||
|
||||
from .utils import authz_restrict_to_syscom, is_truthy, \
|
||||
create_streaming_response, development_only
|
||||
from ceo_common.interfaces import ILDAPService
|
||||
from ceo_common.utils import fuzzy_result, fuzzy_match
|
||||
from ceod.transactions.groups import (
|
||||
AddGroupTransaction,
|
||||
AddMemberToGroupTransaction,
|
||||
|
@ -11,6 +13,8 @@ from ceod.transactions.groups import (
|
|||
DeleteGroupTransaction,
|
||||
)
|
||||
|
||||
from heapq import heappushpop, nlargest
|
||||
|
||||
bp = Blueprint('groups', __name__)
|
||||
|
||||
|
||||
|
@ -32,6 +36,22 @@ def get_group(group_name):
|
|||
return group.to_dict()
|
||||
|
||||
|
||||
@bp.route('/search/<query>/<count>')
|
||||
def search_group(query, count):
|
||||
query = str(query)
|
||||
count = int(count)
|
||||
ldap_srv = component.getUtility(ILDAPService)
|
||||
clubs = ldap_srv.get_clubs()
|
||||
scores = [fuzzy_result("", 99999) for _ in range(count)]
|
||||
for club in clubs:
|
||||
score = fuzzy_match(query, str(club.cn))
|
||||
result = fuzzy_result(str(club.cn), score)
|
||||
heappushpop(scores, result)
|
||||
|
||||
result = [score.string for score in nlargest(count, scores)]
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@bp.route('/<group_name>/members/<username>', methods=['POST'])
|
||||
@authz_restrict_to_syscom
|
||||
def add_member_to_group(group_name, username):
|
||||
|
|
|
@ -71,7 +71,9 @@ def get_user(auth_user: str, username: str):
|
|||
get_forwarding_addresses = True
|
||||
ldap_srv = component.getUtility(ILDAPService)
|
||||
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'])
|
||||
|
@ -119,9 +121,9 @@ def renew_user(username: str):
|
|||
user.set_expired(False)
|
||||
try:
|
||||
user.subscribe_to_mailing_list(member_list)
|
||||
logger.debug(f'Unsubscribed {user.uid} from {member_list}')
|
||||
logger.debug(f'Subscribed {user.uid} to {member_list}')
|
||||
except UserAlreadySubscribedError:
|
||||
logger.debug(f'{user.uid} is already unsubscribed from {member_list}')
|
||||
logger.debug(f'{user.uid} is already subscribed to {member_list}')
|
||||
|
||||
if terms:
|
||||
logger.info(f"Renewing member {username} for terms {terms}")
|
||||
|
|
|
@ -2,6 +2,7 @@ from flask import Blueprint, request
|
|||
from zope import component
|
||||
|
||||
from .utils import authz_restrict_to_syscom, create_streaming_response
|
||||
from ceo_common.errors import BadRequest
|
||||
from ceo_common.interfaces import ILDAPService, IConfig
|
||||
from ceod.transactions.members import UpdateMemberPositionsTransaction
|
||||
|
||||
|
@ -15,7 +16,9 @@ def get_positions():
|
|||
positions = {}
|
||||
for user in ldap_srv.get_users_with_positions():
|
||||
for position in user.positions:
|
||||
positions[position] = user.uid
|
||||
if position not in positions:
|
||||
positions[position] = []
|
||||
positions[position].append(user.uid)
|
||||
|
||||
return positions
|
||||
|
||||
|
@ -29,23 +32,31 @@ def update_positions():
|
|||
required = cfg.get('positions_required')
|
||||
available = cfg.get('positions_available')
|
||||
|
||||
# remove falsy values
|
||||
body = {
|
||||
positions: username for positions, username in body.items()
|
||||
if username
|
||||
}
|
||||
# remove falsy values and parse multiple users in each position
|
||||
# Example: "user1,user2, user3" -> ["user1","user2","user3"]
|
||||
position_to_usernames = {}
|
||||
for position, usernames in body.items():
|
||||
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:
|
||||
return {
|
||||
'error': f'unknown position: {position}'
|
||||
}, 400
|
||||
raise BadRequest(f'unknown position: {position}')
|
||||
|
||||
for position in required:
|
||||
if position not in body:
|
||||
return {
|
||||
'error': f'missing required position: {position}'
|
||||
}, 400
|
||||
if position not in position_to_usernames:
|
||||
raise BadRequest(f'missing required position: {position}')
|
||||
|
||||
txn = UpdateMemberPositionsTransaction(body)
|
||||
txn = UpdateMemberPositionsTransaction(position_to_usernames)
|
||||
return create_streaming_response(txn)
|
||||
|
|
|
@ -62,6 +62,7 @@ class CloudStackService:
|
|||
domain_id = self._get_domain_id()
|
||||
url = self._create_url({
|
||||
'command': 'listAccounts',
|
||||
'accounttype': '0', # regular user (exclude domain admin)
|
||||
'domainid': domain_id,
|
||||
'details': 'min',
|
||||
})
|
||||
|
|
|
@ -103,7 +103,7 @@ class LDAPService:
|
|||
conn.search(self.ldap_groups_base,
|
||||
f'(uniqueMember={self.uid_to_dn(username)})',
|
||||
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]]:
|
||||
if not usernames:
|
||||
|
@ -316,7 +316,9 @@ class LDAPService:
|
|||
self,
|
||||
dry_run: bool = False,
|
||||
members: Union[List[str], None] = None,
|
||||
uwldap_batch_size: int = 100,
|
||||
# The UWLDAP server currently has a result set limit of 50
|
||||
# Keep it low just to be safe
|
||||
uwldap_batch_size: int = 10,
|
||||
):
|
||||
if members:
|
||||
filter = '(|' + ''.join([f'(uid={uid})' for uid in members]) + ')'
|
||||
|
@ -358,7 +360,11 @@ class LDAPService:
|
|||
return users_to_change
|
||||
|
||||
def _get_club_uids(self, conn: ldap3.Connection) -> List[str]:
|
||||
conn.search(self.ldap_users_base, '(objectClass=club)', attributes=['uid'])
|
||||
conn.extend.standard.paged_search(self.ldap_users_base,
|
||||
'(objectClass=club)',
|
||||
attributes=['uid'],
|
||||
paged_size=50,
|
||||
generator=False)
|
||||
return [entry.uid.value for entry in conn.entries]
|
||||
|
||||
def get_clubs(self) -> List[IGroup]:
|
||||
|
|
|
@ -4,7 +4,7 @@ import os
|
|||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from typing import List, Dict, Tuple
|
||||
from typing import List, Dict, Tuple, Union
|
||||
|
||||
import jinja2
|
||||
from zope import component
|
||||
|
@ -53,6 +53,7 @@ class VHostManager:
|
|||
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_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_dir = '/root/.acme.sh'
|
||||
|
@ -82,12 +83,12 @@ class VHostManager:
|
|||
"""Return a list of all vhost files for this user."""
|
||||
return glob.glob(os.path.join(self.vhost_dir, username + '_*'))
|
||||
|
||||
def _run(self, args: List[str]):
|
||||
subprocess.run(args, check=True)
|
||||
def _run(self, args: Union[List[str], str], **kwargs):
|
||||
subprocess.run(args, check=True, **kwargs)
|
||||
|
||||
def _reload_web_server(self):
|
||||
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:
|
||||
if VALID_DOMAIN_RE.match(domain) is None:
|
||||
|
@ -150,7 +151,7 @@ class VHostManager:
|
|||
self.acme_sh, '--install-cert', '-d', domain,
|
||||
'--key-file', key_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):
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from collections import defaultdict
|
||||
from typing import Dict
|
||||
from typing import Dict, List
|
||||
|
||||
from zope import component
|
||||
|
||||
|
@ -20,15 +20,19 @@ class UpdateMemberPositionsTransaction(AbstractTransaction):
|
|||
'subscribe_to_mailing_lists',
|
||||
]
|
||||
|
||||
def __init__(self, positions_reversed: Dict[str, str]):
|
||||
def __init__(self, position_to_usernames: Dict[str, List[str]]):
|
||||
# positions_reversed is position -> username
|
||||
super().__init__()
|
||||
self.ldap_srv = component.getUtility(ILDAPService)
|
||||
|
||||
# Reverse the dict so it's easier to use (username -> positions)
|
||||
self.positions = defaultdict(list)
|
||||
for position, username in positions_reversed.items():
|
||||
self.positions[username].append(position)
|
||||
for position, usernames in position_to_usernames.items():
|
||||
if isinstance(usernames, list):
|
||||
for username in usernames:
|
||||
self.positions[username].append(position)
|
||||
else:
|
||||
raise TypeError("Username(s) under each position must be a list")
|
||||
|
||||
# a cached Dict of the Users who need to be modified (username -> User)
|
||||
self.users: Dict[str, IUser] = {}
|
||||
|
@ -42,7 +46,7 @@ class UpdateMemberPositionsTransaction(AbstractTransaction):
|
|||
mailing_lists = cfg.get('auxiliary mailing lists_exec')
|
||||
|
||||
# position -> username
|
||||
new_positions_reversed = {} # For returning result
|
||||
new_position_to_usernames = {} # For returning result
|
||||
|
||||
# retrieve User objects and cache them
|
||||
for username in self.positions:
|
||||
|
@ -64,7 +68,9 @@ class UpdateMemberPositionsTransaction(AbstractTransaction):
|
|||
|
||||
self.old_positions[username] = old_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'
|
||||
|
||||
# update exec group in LDAP
|
||||
|
@ -97,7 +103,7 @@ class UpdateMemberPositionsTransaction(AbstractTransaction):
|
|||
else:
|
||||
yield 'subscribe_to_mailing_lists'
|
||||
|
||||
self.finish(new_positions_reversed)
|
||||
self.finish(new_position_to_usernames)
|
||||
|
||||
def rollback(self):
|
||||
if 'update_exec_group_ldap' in self.finished_operations:
|
||||
|
|
|
@ -1,3 +1,17 @@
|
|||
ceo (1.0.26-bullseye1.1) bullseye; urgency=high
|
||||
|
||||
* Reduce UWLDAP batch size from 100 to 10
|
||||
|
||||
-- Max Erenberg <merenber@csclub.uwaterloo.ca> Mon, 13 Feb 2023 22:35:47 +0000
|
||||
|
||||
ceo (1.0.25-bullseye1.1) bullseye; urgency=medium
|
||||
|
||||
* Support multiple users sharing the same position
|
||||
* Show groups when retrieving user information
|
||||
* Use admin Kerberos credentials when subscribing new member to csc-general
|
||||
|
||||
-- Max Erenberg <merenber@csclub.uwaterloo.ca> Mon, 06 Feb 2023 05:01:46 +0000
|
||||
|
||||
ceo (1.0.24-bullseye1) bullseye; urgency=high
|
||||
|
||||
* Add support for using number in member terms renwewal API
|
||||
|
|
|
@ -1 +1 @@
|
|||
10
|
||||
13
|
||||
|
|
|
@ -2,28 +2,27 @@ Source: ceo
|
|||
Maintainer: Systems Committee <syscom@csclub.uwaterloo.ca>
|
||||
Section: admin
|
||||
Priority: optional
|
||||
Standards-Version: 4.3.0
|
||||
Standards-Version: 4.6.2
|
||||
Vcs-Git: https://git.csclub.uwaterloo.ca/public/pyceo.git
|
||||
Vcs-Browser: https://git.csclub.uwaterloo.ca/public/pyceo
|
||||
Uploaders: Max Erenberg <merenber@csclub.uwaterloo.ca>,
|
||||
Raymond Li <raymo@csclub.uwaterloo.ca>,
|
||||
Edwin <e42zhang@csclub.uwaterloo.ca>
|
||||
Build-Depends: debhelper (>= 12.1.1),
|
||||
python3-dev (>= 3.7),
|
||||
python3-venv (>= 3.7),
|
||||
libkrb5-dev (>= 1.17),
|
||||
libpq-dev (>= 11.13),
|
||||
libaugeas0 (>= 1.11),
|
||||
scdoc (>= 1.9)
|
||||
Build-Depends: debhelper (>= 13),
|
||||
python3-dev (>= 3.9),
|
||||
python3-venv (>= 3.9),
|
||||
libkrb5-dev (>= 1.18),
|
||||
libpq-dev (>= 13.9),
|
||||
libaugeas0 (>= 1.12),
|
||||
scdoc (>= 1.11)
|
||||
|
||||
Package: ceo-common
|
||||
Architecture: amd64
|
||||
Depends: python3 (>= 3.7),
|
||||
krb5-user (>= 1.17),
|
||||
libkrb5-3 (>= 1.17),
|
||||
libpq5 (>= 11.13),
|
||||
libaugeas0 (>= 1.11),
|
||||
${python3:Depends},
|
||||
Depends: python3 (>= 3.9),
|
||||
krb5-user (>= 1.18),
|
||||
libkrb5-3 (>= 1.18),
|
||||
libpq5 (>= 13.9),
|
||||
libaugeas0 (>= 1.12),
|
||||
${misc:Depends}
|
||||
Description: CSC Electronic Office common files
|
||||
This package contains the common files for the CSC Electronic Office.
|
||||
|
@ -41,6 +40,7 @@ Package: ceod
|
|||
Architecture: amd64
|
||||
Replaces: ceo-daemon
|
||||
Conflicts: ceo-daemon
|
||||
Pre-Depends: ${misc:Pre-Depends}
|
||||
Depends: ceo-common (= ${source:Version}), openssl (>= 1.1.1), ${misc:Depends}
|
||||
Description: CSC Electronic Office daemon
|
||||
This package contains the daemon for the CSC Electronic Office.
|
||||
|
|
|
@ -5,5 +5,10 @@
|
|||
|
||||
override_dh_strip:
|
||||
|
||||
override_dh_strip_nondeterminism:
|
||||
|
||||
override_dh_shlibdeps:
|
||||
|
||||
override_dh_makeshlibs:
|
||||
|
||||
override_dh_dwz:
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
version: "3.6"
|
||||
|
||||
x-common: &common
|
||||
image: python:3.7-buster
|
||||
image: python:3.9-bullseye
|
||||
volumes:
|
||||
- .:$PWD:z
|
||||
security_opt:
|
||||
- label:disable
|
||||
environment:
|
||||
FLASK_APP: ceod.api
|
||||
FLASK_ENV: development
|
||||
|
@ -14,7 +16,7 @@ x-common: &common
|
|||
services:
|
||||
auth1:
|
||||
<<: *common
|
||||
image: debian:buster
|
||||
image: debian:bullseye
|
||||
hostname: auth1
|
||||
command: auth1
|
||||
|
||||
|
|
|
@ -61,9 +61,9 @@ paths:
|
|||
program:
|
||||
$ref: "#/components/schemas/Program"
|
||||
terms:
|
||||
$ref: "#/components/schemas/Terms"
|
||||
$ref: "#/components/schemas/TermsOrNumTerms"
|
||||
non_member_terms:
|
||||
$ref: "#/components/schemas/NonMemberTerms"
|
||||
$ref: "#/components/schemas/TermsOrNumTerms"
|
||||
forwarding_addresses:
|
||||
$ref: "#/components/schemas/ForwardingAddresses"
|
||||
responses:
|
||||
|
@ -161,17 +161,11 @@ paths:
|
|||
- type: object
|
||||
properties:
|
||||
terms:
|
||||
type: array
|
||||
description: Terms for which this user will be a member
|
||||
items:
|
||||
$ref: "#/components/schemas/Term"
|
||||
$ref: "#/components/schemas/TermsOrNumTerms"
|
||||
- type: object
|
||||
properties:
|
||||
non_member_terms:
|
||||
type: array
|
||||
description: Terms for which this user will be a club rep
|
||||
items:
|
||||
$ref: "#/components/schemas/Term"
|
||||
$ref: "#/components/schemas/TermsOrNumTerms"
|
||||
example: {"terms": ["f2021"]}
|
||||
responses:
|
||||
"200":
|
||||
|
@ -290,6 +284,26 @@ paths:
|
|||
$ref: "#/components/schemas/UID"
|
||||
"404":
|
||||
$ref: "#/components/responses/GroupNotFoundErrorResponse"
|
||||
/groups/{query}/{count}:
|
||||
get:
|
||||
tags: ['groups']
|
||||
summary: fuzzy search groups
|
||||
description: >-
|
||||
search count number of groups, returns a list of names sorted by levenshtein edit
|
||||
distance.
|
||||
parameters:
|
||||
- name: query
|
||||
in: path
|
||||
description: query or string to search for
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: count
|
||||
in: path
|
||||
description: number of results to return, returns empty strings if necessary
|
||||
required: true
|
||||
schema:
|
||||
type: int
|
||||
/groups/{group_name}/members/{username}:
|
||||
post:
|
||||
tags: ['groups']
|
||||
|
@ -383,11 +397,14 @@ paths:
|
|||
schema:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: array
|
||||
description: list of usernames
|
||||
items:
|
||||
type: string
|
||||
example:
|
||||
president: user0
|
||||
vice-president: user1
|
||||
sysadmin: user2
|
||||
president: ["user1"]
|
||||
vice-president: ["user2", "user3"]
|
||||
sysadmin: ["user4"]
|
||||
treasurer:
|
||||
post:
|
||||
tags: ['positions']
|
||||
|
@ -404,11 +421,18 @@ paths:
|
|||
schema:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
oneOf:
|
||||
- type: string
|
||||
description: username or comma-separated list of usernames
|
||||
- type: array
|
||||
description: list of usernames
|
||||
items:
|
||||
type: string
|
||||
example:
|
||||
president: user0
|
||||
vice-president: user1
|
||||
sysadmin: user2
|
||||
president: user1
|
||||
vice-president: user2, user3
|
||||
secretary: ["user4", "user5"]
|
||||
sysadmin: ["user6"]
|
||||
treasurer:
|
||||
responses:
|
||||
"200":
|
||||
|
@ -422,7 +446,7 @@ paths:
|
|||
{"status": "in progress", "operation": "update_positions_ldap"}
|
||||
{"status": "in progress", "operation": "update_exec_group_ldap"}
|
||||
{"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":
|
||||
description: Failed
|
||||
content:
|
||||
|
@ -883,14 +907,15 @@ components:
|
|||
example: MAT/Mathematics Computer Science
|
||||
Terms:
|
||||
type: array
|
||||
description: Terms for which this user was a member
|
||||
items:
|
||||
$ref: "#/components/schemas/Term"
|
||||
NonMemberTerms:
|
||||
type: array
|
||||
description: Terms for which this user was a club rep
|
||||
description: List of terms
|
||||
items:
|
||||
$ref: "#/components/schemas/Term"
|
||||
TermsOrNumTerms:
|
||||
oneOf:
|
||||
- type: integer
|
||||
description: number of additional terms to add
|
||||
example: 1
|
||||
- $ref: "#/components/schemas/Terms"
|
||||
LoginShell:
|
||||
type: string
|
||||
description: Login shell
|
||||
|
@ -939,9 +964,14 @@ components:
|
|||
terms:
|
||||
$ref: "#/components/schemas/Terms"
|
||||
non_member_terms:
|
||||
$ref: "#/components/schemas/NonMemberTerms"
|
||||
$ref: "#/components/schemas/Terms"
|
||||
forwarding_addresses:
|
||||
$ref: "#/components/schemas/ForwardingAddresses"
|
||||
groups:
|
||||
type: array
|
||||
description: Groups for which this user is a member of
|
||||
items:
|
||||
$ref: "#/components/schemas/GroupCN"
|
||||
UWLDAPUser:
|
||||
type: object
|
||||
properties:
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -97,6 +97,7 @@ members_domain = csclub.cloud
|
|||
k8s_members_domain = k8s.csclub.cloud
|
||||
ip_range_min = 172.19.134.10
|
||||
ip_range_max = 172.19.134.160
|
||||
reload_web_server_cmd = /root/bin/reload-nginx.sh
|
||||
|
||||
[k8s]
|
||||
members_clusterrole = csc-members-default
|
||||
|
|
|
@ -6,7 +6,7 @@ gunicorn==20.1.0
|
|||
Jinja2==3.1.2
|
||||
ldap3==2.9.1
|
||||
mysql-connector-python==8.0.26
|
||||
psycopg2==2.9.1
|
||||
psycopg2-binary==2.9.1
|
||||
python-augeas==1.1.0
|
||||
requests==2.26.0
|
||||
requests-gssapi==1.2.3
|
||||
|
|
|
@ -17,6 +17,11 @@ def test_cloud_account_activate(cli_setup, mock_cloud_server, new_user, cfg):
|
|||
'Congratulations! Your cloud account has been activated.\n'
|
||||
f'You may now login into https://cloud.{base_domain} with your CSC credentials.\n'
|
||||
"Make sure to enter 'Members' for the domain (no quotes).\n"
|
||||
'\n'
|
||||
'Please note that your cloud account will be PERMANENTLY DELETED when\n'
|
||||
'your CSC membership expires, so make sure to purchase enough membership\n'
|
||||
'terms in advance. You will receive a warning email one week before your\n'
|
||||
'cloud account is deleted, so please make sure to check your Junk folder.\n'
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert result.output == expected
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import os
|
||||
import shutil
|
||||
import time
|
||||
|
||||
from click.testing import CliRunner
|
||||
from mysql.connector import connect
|
||||
|
@ -10,10 +11,15 @@ from ceo.cli import cli
|
|||
|
||||
|
||||
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(
|
||||
host=host,
|
||||
user=username,
|
||||
password=password,
|
||||
host=host,
|
||||
user=username,
|
||||
password=password,
|
||||
) as con, con.cursor() as cur:
|
||||
cur.execute("SHOW DATABASES")
|
||||
response = cur.fetchall()
|
||||
|
@ -34,7 +40,7 @@ def test_mysql(cli_setup, cfg, ldap_user):
|
|||
|
||||
# create database for user
|
||||
result = runner.invoke(cli, ['mysql', 'create', username], input='y\n')
|
||||
print(result.output)
|
||||
#print(result.output) # noqa: E265
|
||||
assert result.exit_code == 0
|
||||
assert os.path.isfile(info_file_path)
|
||||
|
||||
|
|
|
@ -77,6 +77,44 @@ def test_groups(cli_setup, ldap_user):
|
|||
result = runner.invoke(cli, ['groups', 'delete', 'test_group_1'], input='y\n')
|
||||
assert result.exit_code == 0
|
||||
|
||||
group_names = [
|
||||
"touch",
|
||||
"error",
|
||||
"happy",
|
||||
"moon",
|
||||
"decisive",
|
||||
"exciting",
|
||||
"super",
|
||||
"ambitious",
|
||||
"acidic",
|
||||
"addition",
|
||||
"blue-eyed",
|
||||
"grate",
|
||||
"replace",
|
||||
"natural",
|
||||
"explode",
|
||||
"decorous",
|
||||
"wide",
|
||||
"hang",
|
||||
"tomatoes",
|
||||
"thirsty",
|
||||
]
|
||||
runner = CliRunner()
|
||||
for name in group_names:
|
||||
result = runner.invoke(cli, [
|
||||
'groups', 'add', name, '-d', 'searchable group',
|
||||
], input='y\n')
|
||||
assert result.exit_code == 0
|
||||
|
||||
for name in group_names:
|
||||
result = runner.invoke(cli, ['groups', 'search', '--count=1', name])
|
||||
assert result.exit_code == 0
|
||||
assert result.output.find(name) != -1
|
||||
|
||||
for name in group_names:
|
||||
result = runner.invoke(cli, ['groups', 'delete', name], input='y\n')
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
def create_group(group_name, desc):
|
||||
runner = CliRunner()
|
||||
|
|
|
@ -26,8 +26,9 @@ def test_members_get(cli_setup, ldap_user):
|
|||
f"home directory: {ldap_user.home_directory}\n"
|
||||
f"is a club: {ldap_user.is_club()}\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"groups:\n"
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert result.output == expected
|
||||
|
|
|
@ -44,17 +44,17 @@ vice-president: test_1
|
|||
sysadmin: test_2
|
||||
secretary: test_3
|
||||
webmaster: test_4
|
||||
treasurer:
|
||||
cro:
|
||||
librarian:
|
||||
imapd:
|
||||
offsck:
|
||||
treasurer:
|
||||
cro:
|
||||
librarian:
|
||||
imapd:
|
||||
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:] # noqa: W291
|
||||
'''[1:]
|
||||
|
||||
result = runner.invoke(cli, ['positions', 'get'])
|
||||
assert result.exit_code == 0
|
||||
|
@ -71,3 +71,71 @@ webmaster: test_4
|
|||
for user in users:
|
||||
user.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()
|
||||
|
|
|
@ -111,6 +111,206 @@ def test_api_add_member_to_group(client, create_group_result, ldap_user):
|
|||
assert data['members'] == []
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def create_random_names():
|
||||
# 150 random names generated with https://www.randomlists.com/random-words
|
||||
random_names = [
|
||||
"intelligent",
|
||||
"skin",
|
||||
"shivering",
|
||||
"hapless",
|
||||
"abstracted",
|
||||
"kiss",
|
||||
"decision",
|
||||
"van",
|
||||
"advise",
|
||||
"parcel",
|
||||
"disillusioned",
|
||||
"print",
|
||||
"skate",
|
||||
"robin",
|
||||
"explode",
|
||||
"fearless",
|
||||
"feeling",
|
||||
"chemical",
|
||||
"identify",
|
||||
"baseball",
|
||||
"room",
|
||||
"contain",
|
||||
"smooth",
|
||||
"play",
|
||||
"fierce",
|
||||
"north",
|
||||
"secretive",
|
||||
"plug",
|
||||
"rely",
|
||||
"home",
|
||||
"push",
|
||||
"guard",
|
||||
"allow",
|
||||
"depressed",
|
||||
"evasive",
|
||||
"slap",
|
||||
"delicate",
|
||||
"concern",
|
||||
"consider",
|
||||
"fang",
|
||||
"roll",
|
||||
"bait",
|
||||
"rabbits",
|
||||
"guarded",
|
||||
"abnormal",
|
||||
"loutish",
|
||||
"voracious",
|
||||
"chase",
|
||||
"army",
|
||||
"harsh",
|
||||
"grieving",
|
||||
"tacky",
|
||||
"far",
|
||||
"wise",
|
||||
"street",
|
||||
"price",
|
||||
"bikes",
|
||||
"post",
|
||||
"afternoon",
|
||||
"deranged",
|
||||
"cart",
|
||||
"evanescent",
|
||||
"shrill",
|
||||
"uppity",
|
||||
"adhoc",
|
||||
"alleged",
|
||||
"round",
|
||||
"smart",
|
||||
"support",
|
||||
"plantation",
|
||||
"flap",
|
||||
"pretty",
|
||||
"radiate",
|
||||
"excite",
|
||||
"memorize",
|
||||
"whisper",
|
||||
"thoughtless",
|
||||
"substantial",
|
||||
"upset",
|
||||
"pathetic",
|
||||
"flow",
|
||||
"shake",
|
||||
"wail",
|
||||
"share",
|
||||
"songs",
|
||||
"scream",
|
||||
"aspiring",
|
||||
"overwrought",
|
||||
"mass",
|
||||
"romantic",
|
||||
"deliver",
|
||||
"anxious",
|
||||
"laborer",
|
||||
"angry",
|
||||
"faded",
|
||||
"wish",
|
||||
"homeless",
|
||||
"salty",
|
||||
"start",
|
||||
"crooked",
|
||||
"tremble",
|
||||
"enjoy",
|
||||
"chivalrous",
|
||||
"useless",
|
||||
"womanly",
|
||||
"brake",
|
||||
"wandering",
|
||||
"please",
|
||||
"cow",
|
||||
"reason",
|
||||
"expert",
|
||||
"null",
|
||||
"basket",
|
||||
"early",
|
||||
"river",
|
||||
"prevent",
|
||||
"sticks",
|
||||
"vacation",
|
||||
"eggnog",
|
||||
"receive",
|
||||
"memory",
|
||||
"exchange",
|
||||
"burly",
|
||||
"agreement",
|
||||
"flock",
|
||||
"subdued",
|
||||
"clap",
|
||||
"simplistic",
|
||||
"tiger",
|
||||
"responsible",
|
||||
"knock",
|
||||
"camera",
|
||||
"nifty",
|
||||
"capable",
|
||||
"disappear",
|
||||
"afterthought",
|
||||
"obese",
|
||||
"harass",
|
||||
"delicious",
|
||||
"badge",
|
||||
"dam",
|
||||
"plate",
|
||||
"acrid",
|
||||
"voiceless",
|
||||
"mate",
|
||||
"juice",
|
||||
"food",
|
||||
"town",
|
||||
"giraffe",
|
||||
"decorate"
|
||||
]
|
||||
yield random_names
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def create_searchable_groups(client, create_random_names):
|
||||
random_names = create_random_names
|
||||
for name in random_names:
|
||||
status, data = client.post('/api/groups', json={
|
||||
'cn': name,
|
||||
'description': 'Groups with distinct names for testing searching',
|
||||
})
|
||||
assert status == 200
|
||||
assert data[-1]['status'] == 'completed'
|
||||
yield random_names
|
||||
|
||||
|
||||
def test_api_group_search(client, create_searchable_groups):
|
||||
cns = create_searchable_groups
|
||||
# pairs of cn indices as well as amount of results that should be returned
|
||||
random_numbers = [
|
||||
(88, 68),
|
||||
(117, 54),
|
||||
(63, 97),
|
||||
(64, 19),
|
||||
(114, 98),
|
||||
(45, 146),
|
||||
(58, 12),
|
||||
(42, 126),
|
||||
(66, 137),
|
||||
(39, 135),
|
||||
]
|
||||
|
||||
for tup in random_numbers:
|
||||
cn = cns[tup[0]]
|
||||
status, data = client.get(f'/api/groups/search/{cn}/{tup[1]}')
|
||||
assert status == 200
|
||||
assert len(data) == tup[1]
|
||||
assert data[0] == cn
|
||||
|
||||
for cn in cns:
|
||||
status, data = client.delete(f'/api/groups/{cn}')
|
||||
assert status == 200
|
||||
assert data[-1]['status'] == 'completed'
|
||||
|
||||
|
||||
def test_api_group_auxiliary(cfg, client, ldap_user, g_admin_ctx):
|
||||
# Make sure that syscom has auxiliary mailing lists and groups
|
||||
# defined in ceod_test_local.ini.
|
||||
|
|
|
@ -127,6 +127,7 @@ def test_api_get_user(cfg, client, create_user_result):
|
|||
del old_data['password']
|
||||
|
||||
status, data = client.get(f'/api/members/{uid}')
|
||||
del data['groups']
|
||||
assert status == 200
|
||||
assert data == old_data
|
||||
|
||||
|
@ -262,6 +263,7 @@ def test_authz_check(client, create_user_result):
|
|||
del old_data['password']
|
||||
del old_data['forwarding_addresses']
|
||||
_, data = client.get(f'/api/members/{uid}', principal='regular1')
|
||||
del data['groups']
|
||||
assert data == old_data
|
||||
|
||||
# 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')
|
||||
assert status == 200
|
||||
expected = {
|
||||
'president': ldap_user.uid,
|
||||
'treasurer': ldap_user.uid,
|
||||
'president': [ldap_user.uid],
|
||||
'treasurer': [ldap_user.uid],
|
||||
}
|
||||
assert data == expected
|
||||
|
||||
|
@ -20,7 +20,7 @@ def test_set_positions(cfg, client, g_admin_ctx, mock_mailman_server):
|
|||
|
||||
users = []
|
||||
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',
|
||||
terms=['s2021'])
|
||||
user.add_to_ldap()
|
||||
|
@ -31,23 +31,23 @@ def test_set_positions(cfg, client, g_admin_ctx, mock_mailman_server):
|
|||
try:
|
||||
# missing required position
|
||||
status, _ = client.post('/api/positions', json={
|
||||
'vice-president': 'test_1',
|
||||
'vice-president': 'test1',
|
||||
})
|
||||
assert status == 400
|
||||
|
||||
# non-existent position
|
||||
status, _ = client.post('/api/positions', json={
|
||||
'president': 'test_1',
|
||||
'vice-president': 'test_2',
|
||||
'sysadmin': 'test_3',
|
||||
'no-such-position': 'test_3',
|
||||
'president': 'test1',
|
||||
'vice-president': 'test2',
|
||||
'sysadmin': 'test3',
|
||||
'no-such-position': 'test3',
|
||||
})
|
||||
assert status == 400
|
||||
|
||||
status, data = client.post('/api/positions', json={
|
||||
'president': 'test_1',
|
||||
'vice-president': 'test_2',
|
||||
'sysadmin': 'test_3',
|
||||
'president': 'test1',
|
||||
'vice-president': 'test2',
|
||||
'sysadmin': 'test3',
|
||||
})
|
||||
assert status == 200
|
||||
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": "subscribe_to_mailing_lists"},
|
||||
{"status": "completed", "result": {
|
||||
"president": "test_1",
|
||||
"vice-president": "test_2",
|
||||
"sysadmin": "test_3",
|
||||
"president": ["test1"],
|
||||
"vice-president": ["test2"],
|
||||
"sysadmin": ["test3"],
|
||||
}},
|
||||
]
|
||||
assert data == expected
|
||||
# make sure execs were added to exec group
|
||||
status, data = client.get('/api/groups/exec')
|
||||
assert status == 200
|
||||
expected = ['test_1', 'test_2', 'test_3']
|
||||
expected = ['test1', 'test2', 'test3']
|
||||
assert sorted([item['uid'] for item in data['members']]) == expected
|
||||
# make sure execs were subscribed to mailing lists
|
||||
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
|
||||
|
||||
_, data = client.post('/api/positions', json={
|
||||
'president': 'test_1',
|
||||
'vice-president': 'test_2',
|
||||
'sysadmin': 'test_2',
|
||||
'treasurer': 'test_4',
|
||||
'president': 'test1',
|
||||
'vice-president': 'test2',
|
||||
'sysadmin': 'test2',
|
||||
'treasurer': 'test4',
|
||||
})
|
||||
assert data[-1]['status'] == 'completed'
|
||||
# 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')
|
||||
assert sorted([item['uid'] for item in data['members']]) == expected
|
||||
# make sure old exec was removed from mailing lists
|
||||
addresses = [f'{uid}@{base_domain}' for uid in expected]
|
||||
for mailing_list in mailing_lists:
|
||||
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:
|
||||
with g_admin_ctx():
|
||||
for user in users:
|
||||
|
|
|
@ -91,6 +91,7 @@ members_domain = csclub.cloud
|
|||
k8s_members_domain = k8s.csclub.cloud
|
||||
ip_range_min = 172.19.134.10
|
||||
ip_range_max = 172.19.134.160
|
||||
reload_web_server_cmd = systemctl reload nginx
|
||||
|
||||
[k8s]
|
||||
members_clusterrole = csc-members-default
|
||||
|
|
|
@ -90,6 +90,7 @@ members_domain = csclub.cloud
|
|||
k8s_members_domain = k8s.csclub.cloud
|
||||
ip_range_min = 172.19.134.10
|
||||
ip_range_max = 172.19.134.160
|
||||
reload_web_server_cmd = systemctl reload nginx
|
||||
|
||||
[k8s]
|
||||
members_clusterrole = csc-members-default
|
||||
|
|
Loading…
Reference in New Issue