pyceo/tests/ceod/api/test_members.py

324 lines
11 KiB
Python

from unittest.mock import patch
import ldap3
import pytest
import datetime
import ceod.utils
import ceo_common.utils
from ceo_common.model import Term
def test_api_user_not_found(client):
status, data = client.get('/api/members/no_such_user')
assert status == 404
@pytest.fixture(scope='module')
def create_user_resp(client, mocks_for_create_user_module, mock_mail_server):
mock_mail_server.messages.clear()
status, data = client.post('/api/members', json={
'uid': 'test1',
'cn': 'Test One',
'given_name': 'Test',
'sn': 'One',
'program': 'Math',
'terms': ['s2021'],
'forwarding_addresses': ['test1@uwaterloo.internal'],
})
assert status == 200
assert data[-1]['status'] == 'completed'
yield status, data
status, data = client.delete('/api/members/test1')
assert status == 200
assert data[-1]['status'] == 'completed'
@pytest.fixture(scope='function')
def create_user_result(create_user_resp):
# convenience method
_, data = create_user_resp
return data[-1]['result']
def test_api_create_user(cfg, create_user_resp, mock_mail_server):
_, data = create_user_resp
min_uid = cfg.get('members_min_id')
expected = [
{"status": "in progress", "operation": "add_user_to_ldap"},
{"status": "in progress", "operation": "add_group_to_ldap"},
{"status": "in progress", "operation": "add_user_to_kerberos"},
{"status": "in progress", "operation": "create_home_dir"},
{"status": "in progress", "operation": "set_forwarding_addresses"},
{"status": "in progress", "operation": "send_welcome_message"},
{"status": "in progress", "operation": "subscribe_to_mailing_list"},
{"status": "in progress", "operation": "announce_new_user"},
{"status": "completed", "result": {
"cn": "Test One",
"given_name": "Test",
"sn": "One",
"uid": "test1",
"uid_number": min_uid,
"gid_number": min_uid,
"login_shell": "/bin/bash",
"home_directory": "/tmp/test_users/test1",
"is_club": False,
"is_club_rep": False,
"program": "Math",
"terms": ["s2021"],
"mail_local_addresses": ["test1@csclub.internal"],
"forwarding_addresses": ['test1@uwaterloo.internal'],
"password": "krb5",
"shadowExpire": None,
}},
]
assert data == expected
# Two messages should have been sent: a welcome message to the new member,
# and an announcement to the ceo mailing list
assert len(mock_mail_server.messages) == 2
assert mock_mail_server.messages[0]['to'] == 'test1@csclub.internal'
assert mock_mail_server.messages[1]['to'] == 'ceo@csclub.internal,ctdalek@csclub.internal'
mock_mail_server.messages.clear()
def test_api_next_uid(cfg, client, create_user_result):
min_uid = cfg.get('members_min_id')
_, data = client.post('/api/members', json={
'uid': 'test_2',
'cn': 'Test Two',
'given_name': 'Test',
'sn': 'Two',
'program': 'Math',
'terms': ['s2021'],
})
assert data[-1]['status'] == 'completed'
result = data[-1]['result']
try:
assert result['uid_number'] == min_uid + 1
assert result['gid_number'] == min_uid + 1
finally:
client.delete('/api/members/test_2')
def test_api_get_user(cfg, client, create_user_result):
old_data = create_user_result.copy()
uid = old_data['uid']
del old_data['password']
status, data = client.get(f'/api/members/{uid}')
assert status == 200
assert data == old_data
def test_api_patch_user(client, create_user_result):
data = create_user_result
uid = data['uid']
prev_login_shell = data['login_shell']
prev_forwarding_addresses = data['forwarding_addresses']
# replace login shell
new_shell = '/bin/sh'
status, data = client.patch(
f'/api/members/{uid}', json={'login_shell': new_shell})
assert status == 200
expected = [
{"status": "in progress", "operation": "replace_login_shell"},
{"status": "completed", "result": "OK"},
]
assert data == expected
# replace forwarding addresses
new_forwarding_addresses = [
'test@example1.internal', 'test@example2.internal',
]
status, data = client.patch(
f'/api/members/{uid}', json={
'forwarding_addresses': new_forwarding_addresses
}
)
assert status == 200
expected = [
{"status": "in progress", "operation": "replace_forwarding_addresses"},
{"status": "completed", "result": "OK"},
]
assert data == expected
# retrieve the user and make sure that both fields were changed
status, data = client.get(f'/api/members/{uid}')
assert status == 200
assert data['login_shell'] == new_shell
assert data['forwarding_addresses'] == new_forwarding_addresses
# replace (restore) both
status, data = client.patch(
f'/api/members/{uid}', json={
'login_shell': prev_login_shell,
'forwarding_addresses': prev_forwarding_addresses
}
)
assert status == 200
expected = [
{"status": "in progress", "operation": "replace_login_shell"},
{"status": "in progress", "operation": "replace_forwarding_addresses"},
{"status": "completed", "result": "OK"},
]
assert data == expected
def test_api_renew_user(cfg, client, create_user_result, ldap_conn):
data = create_user_result.copy()
uid = data['uid']
old_terms = data['terms']
old_non_member_terms = data.get('non_member_terms', [])
new_terms = ['f2021']
status, data = client.post(
f'/api/members/{uid}/renew', json={'terms': new_terms})
assert status == 200
assert data == {'terms_added': new_terms}
new_non_member_terms = ['w2022', 's2022']
status, data = client.post(
f'/api/members/{uid}/renew', json={'non_member_terms': new_non_member_terms})
assert status == 200
assert data == {'non_member_terms_added': new_non_member_terms}
# check that the changes were applied
_, data = client.get(f'/api/members/{uid}')
assert data['terms'] == old_terms + new_terms
assert data['non_member_terms'] == old_non_member_terms + new_non_member_terms
assert data['is_club_rep']
# cleanup
base_dn = cfg.get('ldap_users_base')
dn = f'uid={uid},{base_dn}'
changes = {
'term': [(ldap3.MODIFY_REPLACE, old_terms)],
'nonMemberTerm': [(ldap3.MODIFY_REPLACE, old_non_member_terms)],
'isClubRep': [(ldap3.MODIFY_REPLACE, [])],
}
ldap_conn.modify(dn, changes)
def test_api_reset_password(client, create_user_result):
uid = create_user_result['uid']
with patch.object(ceod.utils, 'gen_password') as gen_password_mock:
gen_password_mock.return_value = 'new_password'
status, data = client.post(f'/api/members/{uid}/pwreset')
assert status == 200
assert data['password'] == 'new_password'
# cleanup
status, data = client.post(f'/api/members/{uid}/pwreset')
assert status == 200
assert data['password'] == 'krb5'
def test_authz_check(client, create_user_result):
# non-staff members may not create users
status, data = client.post('/api/members', json={
'uid': 'test1', 'cn': 'Test One', 'given_name': 'Test',
'sn': 'One', 'terms': ['s2021'],
}, principal='regular1')
assert status == 403
# non-syscom members may not see forwarding addresses
old_data = create_user_result.copy()
uid = old_data['uid']
del old_data['password']
del old_data['forwarding_addresses']
_, data = client.get(f'/api/members/{uid}', principal='regular1')
assert data == old_data
# If we're syscom but we don't pass credentials, the request should fail
_, data = client.post('/api/members', json={
'uid': 'test1', 'cn': 'Test One', 'given_name': 'Test',
'sn': 'One', 'terms': ['s2021'],
}, principal='ctdalek', delegate=False)
assert data[-1]['status'] == 'aborted'
@pytest.mark.parametrize('term_attr', ['terms', 'non_member_terms'])
def test_expire(client, new_user, term_attr, syscom_group, ldap_conn):
assert new_user.shadowExpire is None
current_term = Term.current()
start_of_current_term = current_term.to_datetime()
def reset_terms():
if term_attr == 'terms':
attr = 'term'
else:
attr = 'nonMemberTerm'
changes = {
attr: [(ldap3.MODIFY_REPLACE, [str(current_term)])]
}
dn = new_user.ldap_srv.uid_to_dn(new_user.uid)
ldap_conn.modify(dn, changes)
if term_attr == 'terms':
new_user.terms = [str(current_term)]
else:
new_user.non_member_terms = [str(current_term)]
# test_date, should_expire
test_cases = [
# same term, membership is still valid
(start_of_current_term + datetime.timedelta(days=90), False),
# first month of next term, grace period is activated
(start_of_current_term + datetime.timedelta(days=130), False),
# second month of next term, membership is now invalid
(start_of_current_term + datetime.timedelta(days=160), True),
# next next term, membership is definitely invalid
(start_of_current_term + datetime.timedelta(days=250), True),
]
uid = new_user.uid
for test_date, should_expire in test_cases:
with patch.object(ceo_common.utils, 'get_current_datetime') as datetime_mock:
user = new_user.to_dict()
datetime_mock.return_value = test_date
status, data = client.post('/api/members/expire?dry_run=yes')
assert status == 200
assert (data == [uid]) == should_expire
_, user = client.get(f'/api/members/{uid}')
assert user['shadowExpire'] is None
status, data = client.post('/api/members/expire')
assert status == 200
assert (data == [uid]) == should_expire
_, user = client.get(f'/api/members/{uid}')
assert (user['shadowExpire'] is not None) == should_expire
if not should_expire:
continue
term = Term.from_datetime(test_date)
status, _ = client.post(f'/api/members/{uid}/renew', json={term_attr: [str(term)]})
assert status == 200
_, user = client.get(f'/api/members/{uid}')
assert user['shadowExpire'] is None
reset_terms()
@pytest.mark.parametrize('in_syscom', [True, False])
def test_expire_syscom_member(client, new_user, syscom_group, g_admin_ctx, ldap_conn, in_syscom):
uid = new_user.uid
start_of_current_term = Term.current().to_datetime()
if in_syscom:
group_dn = new_user.ldap_srv.group_cn_to_dn('syscom')
user_dn = new_user.ldap_srv.uid_to_dn(uid)
changes = {
'uniqueMember': [(ldap3.MODIFY_ADD, [user_dn])]
}
ldap_conn.modify(group_dn, changes)
with patch.object(ceo_common.utils, 'get_current_datetime') as datetime_mock:
datetime_mock.return_value = start_of_current_term + datetime.timedelta(days=160)
status, data = client.post('/api/members/expire')
assert status == 200
if in_syscom:
assert data == []
else:
assert data == [uid]