parent
d82b5a763b
commit
dd59bea918
@ -1,3 +1,7 @@ |
||||
__pycache__/ |
||||
/venv/ |
||||
.vscode/ |
||||
/cred |
||||
*.o |
||||
*.so |
||||
/ceo_common/krb5/_krb5.c |
||||
|
@ -0,0 +1,96 @@ |
||||
from cffi import FFI |
||||
|
||||
ffibuilder = FFI() |
||||
|
||||
# Definitions selectively copied from <krb5/krb5.h>. |
||||
# Add more if necessary. |
||||
ffibuilder.cdef(r""" |
||||
# define KV5M_DATA ... |
||||
typedef int32_t krb5_int32; |
||||
typedef krb5_int32 krb5_error_code; |
||||
typedef krb5_error_code krb5_magic; |
||||
struct _krb5_context; |
||||
typedef struct _krb5_context* krb5_context; |
||||
struct _krb5_auth_context; |
||||
typedef struct _krb5_auth_context * krb5_auth_context; |
||||
struct _krb5_ccache; |
||||
typedef struct _krb5_ccache *krb5_ccache; |
||||
typedef struct krb5_principal_data { |
||||
...; |
||||
} krb5_principal_data; |
||||
typedef krb5_principal_data * krb5_principal; |
||||
typedef const krb5_principal_data *krb5_const_principal; |
||||
typedef struct _krb5_creds { |
||||
krb5_principal client; |
||||
krb5_principal server; |
||||
...; |
||||
} krb5_creds; |
||||
typedef struct _krb5_data { |
||||
krb5_magic magic; |
||||
unsigned int length; |
||||
char *data; |
||||
} krb5_data; |
||||
typedef struct krb5_replay_data { |
||||
...; |
||||
} krb5_replay_data; |
||||
|
||||
krb5_error_code krb5_init_context(krb5_context * context); |
||||
void krb5_free_context(krb5_context context); |
||||
krb5_error_code krb5_auth_con_init( |
||||
krb5_context context, krb5_auth_context *auth_context); |
||||
krb5_error_code krb5_auth_con_setflags( |
||||
krb5_context context, krb5_auth_context auth_context, krb5_int32 flags); |
||||
krb5_error_code krb5_auth_con_free( |
||||
krb5_context context, krb5_auth_context auth_context); |
||||
krb5_error_code |
||||
krb5_cc_new_unique(krb5_context context, const char *type, const char *hint, |
||||
krb5_ccache *id); |
||||
krb5_error_code krb5_cc_default(krb5_context context, krb5_ccache *ccache); |
||||
krb5_error_code krb5_cc_resolve( |
||||
krb5_context context, const char * name, krb5_ccache * cache); |
||||
krb5_error_code |
||||
krb5_cc_initialize(krb5_context context, krb5_ccache cache, |
||||
krb5_principal principal); |
||||
krb5_error_code |
||||
krb5_cc_get_principal(krb5_context context, krb5_ccache cache, |
||||
krb5_principal *principal); |
||||
krb5_error_code krb5_cc_store_cred( |
||||
krb5_context context, krb5_ccache cache, krb5_creds *creds); |
||||
krb5_error_code krb5_cc_close(krb5_context context, krb5_ccache cache); |
||||
krb5_error_code krb5_cc_destroy(krb5_context context, krb5_ccache cache); |
||||
krb5_error_code |
||||
krb5_build_principal(krb5_context context, |
||||
krb5_principal * princ, |
||||
unsigned int rlen, |
||||
const char * realm, ...); |
||||
krb5_error_code |
||||
krb5_parse_name(krb5_context context, const char *name, |
||||
krb5_principal *principal_out); |
||||
void krb5_free_principal(krb5_context context, krb5_principal val); |
||||
krb5_error_code |
||||
krb5_rd_cred(krb5_context context, krb5_auth_context auth_context, |
||||
krb5_data *pcreddata, krb5_creds ***pppcreds, |
||||
krb5_replay_data *outdata); |
||||
krb5_error_code krb5_fwd_tgt_creds( |
||||
krb5_context context, krb5_auth_context auth_context, |
||||
const char * rhost, krb5_principal client, krb5_principal server, |
||||
krb5_ccache cc, int forwardable, krb5_data * outbuf); |
||||
void krb5_free_tgt_creds(krb5_context context, krb5_creds ** tgts); |
||||
void krb5_free_data_contents(krb5_context context, krb5_data * val); |
||||
krb5_error_code krb5_unparse_name( |
||||
krb5_context context, krb5_const_principal principal, char **name); |
||||
void krb5_free_unparsed_name(krb5_context context, char *val); |
||||
const char * krb5_get_error_message(krb5_context ctx, krb5_error_code code); |
||||
void krb5_free_error_message(krb5_context ctx, const char * msg); |
||||
""") |
||||
|
||||
ffibuilder.set_source( |
||||
"_krb5", |
||||
""" |
||||
#include <krb5/krb5.h> |
||||
""", |
||||
libraries=['krb5'] |
||||
) |
||||
|
||||
if __name__ == '__main__': |
||||
ffibuilder.compile(verbose=True) |
@ -0,0 +1,201 @@ |
||||
import contextlib |
||||
import functools |
||||
from typing import Union |
||||
|
||||
from ._krb5 import ffi, lib |
||||
from ceo_common.errors import KerberosError |
||||
|
||||
|
||||
def check_rc(k_ctx, rc: int): |
||||
""" |
||||
Check the return code of a krb5 function. |
||||
An exception is raised if rc != 0. |
||||
""" |
||||
if rc == 0: |
||||
return |
||||
c_msg = lib.krb5_get_error_message(k_ctx, rc) |
||||
msg = ffi.string(c_msg).decode() |
||||
lib.krb5_free_error_message(k_ctx, c_msg) |
||||
raise KerberosError(msg) |
||||
|
||||
|
||||
@contextlib.contextmanager |
||||
def get_krb5_context(): |
||||
"""Yields a krb5_context.""" |
||||
k_ctx = None |
||||
try: |
||||
p_k_ctx = ffi.new('krb5_context *') |
||||
rc = lib.krb5_init_context(p_k_ctx) |
||||
k_ctx = p_k_ctx[0] # dereference the pointer |
||||
check_rc(k_ctx, rc) |
||||
yield k_ctx |
||||
finally: |
||||
if k_ctx is not None: |
||||
lib.krb5_free_context(k_ctx) |
||||
|
||||
|
||||
@contextlib.contextmanager |
||||
def get_krb5_auth_context(k_ctx): |
||||
"""Yields a krb5_auth_context.""" |
||||
a_ctx = None |
||||
try: |
||||
p_a_ctx = ffi.new('krb5_auth_context *') |
||||
rc = lib.krb5_auth_con_init(k_ctx, p_a_ctx) |
||||
a_ctx = p_a_ctx[0] |
||||
check_rc(k_ctx, rc) |
||||
# clear the flags which enable the replay cache |
||||
rc = lib.krb5_auth_con_setflags(k_ctx, a_ctx, 0) |
||||
check_rc(k_ctx, rc) |
||||
yield a_ctx |
||||
finally: |
||||
if a_ctx is not None: |
||||
lib.krb5_auth_con_free(k_ctx, a_ctx) |
||||
|
||||
|
||||
@contextlib.contextmanager |
||||
def get_krb5_cc_default(k_ctx): |
||||
"""Yields the default krb5_ccache.""" |
||||
cache = None |
||||
try: |
||||
p_cache = ffi.new('krb5_ccache *') |
||||
rc = lib.krb5_cc_default(k_ctx, p_cache) |
||||
check_rc(k_ctx, rc) |
||||
cache = p_cache[0] |
||||
yield cache |
||||
finally: |
||||
if cache is not None: |
||||
lib.krb5_cc_close(k_ctx, cache) |
||||
|
||||
|
||||
@contextlib.contextmanager |
||||
def get_krb5_cc_resolve(k_ctx, name: str): |
||||
""" |
||||
Resolve a credential cache name. |
||||
`name` should have the format 'type:residual'. |
||||
""" |
||||
cache = None |
||||
try: |
||||
c_name = ffi.new('char[]', name.encode()) |
||||
p_cache = ffi.new('krb5_ccache *') |
||||
rc = lib.krb5_cc_resolve(k_ctx, c_name, p_cache) |
||||
check_rc(k_ctx, rc) |
||||
cache = p_cache[0] |
||||
yield cache |
||||
finally: |
||||
if cache is not None: |
||||
lib.krb5_cc_close(k_ctx, cache) |
||||
|
||||
|
||||
def get_fwd_tgt(server: str, cache_name: Union[str, None] = None) -> bytes: |
||||
""" |
||||
Get a forwarded TGT formatted as a KRB-CRED message. |
||||
`server` should have the format 'service/host', |
||||
e.g. 'ceod/phosphoric-acid.csclub.uwaterloo.ca'. |
||||
|
||||
If `cache_name` is None, the default cache will be used; otherwise, |
||||
the cache with that name will be used. |
||||
""" |
||||
if cache_name is None: |
||||
cache_function = get_krb5_cc_default |
||||
else: |
||||
cache_function = functools.partial(get_krb5_cc_resolve, name=cache_name) |
||||
|
||||
with get_krb5_context() as k_ctx, \ |
||||
get_krb5_auth_context(k_ctx) as a_ctx, \ |
||||
cache_function(k_ctx) as cache: |
||||
server_princ = None |
||||
client_princ = None |
||||
outbuf = None |
||||
try: |
||||
# create a server principal from the server string |
||||
c_server = ffi.new('char[]', server.encode()) |
||||
p_server_princ = ffi.new('krb5_principal *') |
||||
rc = lib.krb5_parse_name(k_ctx, c_server, p_server_princ) |
||||
check_rc(k_ctx, rc) |
||||
server_princ = p_server_princ[0] |
||||
# get a client principal from the default cache |
||||
p_client_princ = ffi.new('krb5_principal *') |
||||
rc = lib.krb5_cc_get_principal(k_ctx, cache, p_client_princ) |
||||
check_rc(k_ctx, rc) |
||||
client_princ = p_client_princ[0] |
||||
# get the forwarded TGT |
||||
p_outbuf = ffi.new('krb5_data *') |
||||
rc = lib.krb5_fwd_tgt_creds( |
||||
k_ctx, a_ctx, ffi.NULL, client_princ, server_princ, |
||||
cache, 0, p_outbuf) |
||||
check_rc(k_ctx, rc) |
||||
outbuf = p_outbuf[0] |
||||
return ffi.unpack(outbuf.data, outbuf.length) |
||||
finally: |
||||
if outbuf is not None: |
||||
lib.krb5_free_data_contents(k_ctx, p_outbuf) |
||||
if client_princ is not None: |
||||
lib.krb5_free_principal(k_ctx, client_princ) |
||||
if server_princ is not None: |
||||
lib.krb5_free_principal(k_ctx, server_princ) |
||||
|
||||
|
||||
@contextlib.contextmanager |
||||
def store_fwd_tgt_creds(cred_data_bytes: bytes): |
||||
""" |
||||
Stores the credentials found in cred_data_bytes |
||||
in a new sub-cache in the default cache (which should be of type DIR), |
||||
yields the name of the principal in the credential, then finally removes |
||||
the sub-cache from the collection. |
||||
|
||||
The principal name will have the form 'username@REALM', e.g. |
||||
'ctdalek@CSCLUB.UWATERLOO.CA'. |
||||
""" |
||||
with get_krb5_context() as k_ctx, get_krb5_auth_context(k_ctx) as a_ctx: |
||||
creds_out = None |
||||
cache = None |
||||
c_name = None |
||||
|
||||
try: |
||||
# fill a krb5_data struct |
||||
cred_data = ffi.new('krb5_data *') |
||||
cred_data_buf = ffi.new('char[]', cred_data_bytes) |
||||
cred_data.magic = lib.KV5M_DATA |
||||
cred_data.length = len(cred_data_bytes) |
||||
cred_data.data = cred_data_buf |
||||
|
||||
# read the KRB5-CRED message into a krb5_creds array |
||||
p_creds_out = ffi.new('krb5_creds ***') |
||||
rc = lib.krb5_rd_cred(k_ctx, a_ctx, cred_data, p_creds_out, ffi.NULL) |
||||
check_rc(k_ctx, rc) |
||||
creds_out = p_creds_out[0] |
||||
# there should only be one cred in the array |
||||
cred = creds_out[0] |
||||
|
||||
# read the name of the client principal |
||||
client_princ = cred.client |
||||
p_name = ffi.new('char **') |
||||
rc = lib.krb5_unparse_name(k_ctx, client_princ, p_name) |
||||
check_rc(k_ctx, rc) |
||||
c_name = p_name[0] |
||||
name = ffi.string(c_name).decode() |
||||
|
||||
# create a new cache inside the collection (default cache) |
||||
p_cache = ffi.new('krb5_ccache *') |
||||
cctype = ffi.new('char[]', b'DIR') |
||||
rc = lib.krb5_cc_new_unique(k_ctx, cctype, ffi.NULL, p_cache) |
||||
check_rc(k_ctx, rc) |
||||
cache = p_cache[0] |
||||
# initialize the new cache |
||||
rc = lib.krb5_cc_initialize(k_ctx, cache, client_princ) |
||||
check_rc(k_ctx, rc) |
||||
|
||||
# store the cred into the cache |
||||
rc = lib.krb5_cc_store_cred(k_ctx, cache, cred) |
||||
check_rc(k_ctx, rc) |
||||
|
||||
yield name |
||||
finally: |
||||
if cache is not None: |
||||
# We destroy the cache (instead of closing it) since we want |
||||
# to remove it from disk. |
||||
lib.krb5_cc_destroy(k_ctx, cache) |
||||
if c_name is not None: |
||||
lib.krb5_free_unparsed_name(k_ctx, c_name) |
||||
if creds_out is not None: |
||||
lib.krb5_free_tgt_creds(k_ctx, creds_out) |
@ -0,0 +1,29 @@ |
||||
from base64 import b64decode |
||||
import traceback |
||||
|
||||
from flask import g, request |
||||
|
||||
from ceo_common.logger_factory import logger_factory |
||||
from ceo_common.krb5.utils import store_fwd_tgt_creds |
||||
|
||||
logger = logger_factory(__name__) |
||||
|
||||
|
||||
def before_request(): |
||||
if 'x-krb5-cred' not in request.headers: |
||||
return |
||||
cred = b64decode(request.headers['x-krb5-cred']) |
||||
ctx = store_fwd_tgt_creds(cred) |
||||
name = ctx.__enter__() |
||||
g.stored_creds_ctx = ctx |
||||
g.sasl_user = name |
||||
|
||||
|
||||
def teardown_request(err): |
||||
if 'stored_creds_ctx' not in g: |
||||
return |
||||
try: |
||||
ctx = g.stored_creds_ctx |
||||
ctx.__exit__(None, None, None) |
||||
except Exception: |
||||
logger.error(traceback.format_exc()) |
@ -0,0 +1,47 @@ |
||||
from zope import component |
||||
|
||||
from ..AbstractTransaction import AbstractTransaction |
||||
from ceo_common.errors import UserNotSubscribedError |
||||
from ceo_common.interfaces import ILDAPService, IConfig |
||||
|
||||
|
||||
class DeleteMemberTransaction(AbstractTransaction): |
||||
""" |
||||
Transaction to permanently delete a member and their resources. |
||||
This should only be used during testing. |
||||
""" |
||||
|
||||
operations = [ |
||||
'remove_user_from_ldap', |
||||
'remove_group_from_ldap', |
||||
'remove_user_from_kerberos', |
||||
'delete_home_dir', |
||||
'unsubscribe_from_mailing_list', |
||||
] |
||||
|
||||
def __init__(self, uid: str): |
||||
super().__init__() |
||||
self.uid = uid |
||||
|
||||
def child_execute_iter(self): |
||||
ldap_srv = component.getUtility(ILDAPService) |
||||
cfg = component.getUtility(IConfig) |
||||
user = ldap_srv.get_user(self.uid) |
||||
group = ldap_srv.get_group(self.uid) |
||||
new_member_list = cfg.get('mailman3_new_member_list') |
||||
|
||||
user.remove_from_ldap() |
||||
yield 'remove_user_from_ldap' |
||||
group.remove_from_ldap() |
||||
yield 'remove_group_from_ldap' |
||||
user.remove_from_kerberos() |
||||
yield 'remove_user_from_kerberos' |
||||
user.delete_home_dir() |
||||
yield 'delete_home_dir' |
||||
try: |
||||
user.unsubscribe_from_mailing_list(new_member_list) |
||||
yield 'unsubscribe_from_mailing_list' |
||||
except UserNotSubscribedError: |
||||
pass |
||||
|
||||
self.finish('OK') |
@ -1,27 +0,0 @@ |
||||
from zope import component |
||||
|
||||
from ..AbstractTransaction import AbstractTransaction |
||||
from .utils import gen_password |
||||
from ceo_common.interfaces import ILDAPService |
||||
|
||||
|
||||
class ResetPasswordTransaction(AbstractTransaction): |
||||
"""Transaction to reset a user's password.""" |
||||
|
||||
operations = [ |
||||
'change_password', |
||||
] |
||||
|
||||
def __init__(self, username: str): |
||||
super().__init__() |
||||
self.username = username |
||||
self.ldap_srv = component.getUtility(ILDAPService) |
||||
|
||||
def child_execute_iter(self): |
||||
user = self.ldap_srv.get_user(self.username) |
||||
|
||||
password = gen_password() |
||||
user.change_password(password) |
||||
yield 'change_password' |
||||
|
||||
self.finish({'password': password}) |
@ -1,4 +1,4 @@ |
||||
from .AddMemberTransaction import AddMemberTransaction |
||||
from .ModifyMemberTransaction import ModifyMemberTransaction |
||||
from .RenewMemberTransaction import RenewMemberTransaction |
||||
from .ResetPasswordTransaction import ResetPasswordTransaction |
||||
from .DeleteMemberTransaction import DeleteMemberTransaction |
||||
|
@ -1,20 +0,0 @@ |
||||
from typing import Union, List |
||||
|
||||
from zope import component |
||||
|
||||
from ..AbstractTransaction import AbstractTransaction |
||||
from ceo_common.interfaces import ILDAPService |
||||
|
||||
|
||||
class UpdateProgramsTransaction(AbstractTransaction): |
||||
"""Transaction to sync the 'program' attribute in CSC LDAP with UW LDAP.""" |
||||
|
||||
def __init__(self, members: Union[List[str], None]): |
||||
super().__init__() |
||||
self.members = members |
||||
self.ldap_srv = component.getUtility(ILDAPService) |
||||
|
||||
def child_execute_iter(self): |
||||
users_updated = self.ldap_srv.update_programs(members=self.members) |
||||
yield 'update_programs' |
||||
self.finish(users_updated) |
@ -1 +0,0 @@ |
||||
from .UpdateProgramsTransaction import UpdateProgramsTransaction |
@ -0,0 +1,14 @@ |
||||
#!/usr/bin/env python3 |
||||
|
||||
from base64 import b64encode |
||||
import sys |
||||
|
||||
from ceo_common.krb5.utils import get_fwd_tgt |
||||
|
||||
if len(sys.argv) != 2: |
||||
print(f'Usage: {sys.argv[0]} <ceod hostname>', file=sys.stderr) |
||||
sys.exit(1) |
||||
|
||||
b = get_fwd_tgt('ceod/' + sys.argv[1]) |
||||
with open('cred', 'wb') as f: |
||||
f.write(b64encode(b)) |
@ -1,23 +1,205 @@ |
||||
import pwd |
||||
import grp |
||||
from unittest.mock import patch |
||||
|
||||
import ldap3 |
||||
import pytest |
||||
|
||||
import ceod.utils as utils |
||||
|
||||
|
||||
def test_members_get_user(client): |
||||
def test_api_user_not_found(client): |
||||
status, data = client.get('/api/members/no_such_user') |
||||
assert status == 404 |
||||
assert data['error'] == 'user not found' |
||||
|
||||
|
||||
@pytest.fixture(scope='session') |
||||
def create_user_resp(client): |
||||
return client.post('/api/members', json={ |
||||
'uid': 'test_jdoe', |
||||
'cn': 'John Doe', |
||||
def mocks_for_create_user(): |
||||
with patch.object(utils, 'gen_password') as gen_password_mock, \ |
||||
patch.object(pwd, 'getpwuid') as getpwuid_mock, \ |
||||
patch.object(grp, 'getgrgid') as getgrgid_mock: |
||||
gen_password_mock.return_value = 'krb5' |
||||
# Normally, if getpwuid or getgrgid do *not* raise a KeyError, |
||||
# then LDAPService will skip that UID. Therefore, by raising a |
||||
# KeyError, we are making sure that the UID will *not* be skipped. |
||||
getpwuid_mock.side_effect = KeyError() |
||||
getgrgid_mock.side_effect = KeyError() |
||||
yield |
||||
|
||||
|
||||
@pytest.fixture(scope='session') |
||||
def create_user_resp(client, mocks_for_create_user): |
||||
status, data = client.post('/api/members', json={ |
||||
'uid': 'test_1', |
||||
'cn': 'Test One', |
||||
'program': 'Math', |
||||
'terms': ['s2021'], |
||||
}) |
||||
assert status == 200 |
||||
assert data[-1]['status'] == 'completed' |
||||
yield status, data |
||||
status, data = client.delete('/api/members/test_1') |
||||
assert status == 200 |
||||
assert data[-1]['status'] == 'completed' |
||||
|
||||
|
||||
@pytest.fixture(scope='session') |
||||
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): |
||||
_, 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": "completed", "result": { |
||||
"cn": "Test One", |
||||
"uid": "test_1", |
||||
"uid_number": min_uid, |
||||
"gid_number": min_uid, |
||||
"login_shell": "/bin/bash", |
||||
"home_directory": "/tmp/test_users/test_1", |
||||
"is_club": False, |
||||
"program": "Math", |
||||
"terms": ["s2021"], |
||||
"forwarding_addresses": [], |
||||
"password": "krb5" |
||||
}}, |
||||
] |
||||
assert data == expected |
||||
|
||||
|
||||
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', |
||||
'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 |
||||
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 |
||||
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 |
||||
|
||||
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 |
||||
|
||||
# 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 |
||||
|
||||
# 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)], |
||||
} |
||||
ldap_conn.modify(dn, changes) |
||||
|
||||
|
||||
# def test_create_user(create_user_resp): |
||||
# status, data = create_user_resp |
||||
# assert status == 200 |
||||
# # TODO: check response contents |
||||
def test_api_reset_password(client, create_user_result): |
||||
uid = create_user_result['uid'] |
||||
with patch.object(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' |
||||
|