privacyidea_pam/test.py

651 lines
20 KiB
Python

#!/usr/bin/python3 -W default
#
# This is the test script for libpython-pam. There aren't many stones
# left unturned.
#
# Best run from the Makefile using the target 'test'. To run manually:
# sudo ln -s $PWD/test-pam_python.pam /etc/pam.d
# python test.py
# sudo rm /etc/pam.d/test-pam_python.pam
#
import warnings; warnings.simplefilter('default')
import os
import sys
if sys.hexversion < 0x03000000:
py23_base_exception = Exception
py23_standard_exception = StandardError
def py23_function_name(func):
return func.func_name
else:
py23_base_exception = BaseException
py23_standard_exception = Exception
def py23_function_name(func):
return func.__name__
TEST_PAM_MODULE = "test-pam_python.pam"
TEST_PAM_USER = "root"
#
# A Fairly straight forward test harness.
#
def pam_sm_end(pamh):
return test(pam_sm_end, pamh, None, None)
def pam_sm_authenticate(pamh, flags, argv):
return test(pam_sm_authenticate, pamh, flags, argv)
def pam_sm_setcred(pamh, flags, argv):
return test(pam_sm_setcred, pamh, flags, argv)
def pam_sm_acct_mgmt(pamh, flags, argv):
return test(pam_sm_acct_mgmt, pamh, flags, argv)
def pam_sm_open_session(pamh, flags, argv):
return test(pam_sm_open_session, pamh, flags, argv)
def pam_sm_close_session(pamh, flags, argv):
return test(pam_sm_close_session, pamh, flags, argv)
def pam_sm_chauthtok(pamh, flags, argv):
return test(pam_sm_chauthtok, pamh, flags, argv)
def test(who, pamh, flags, argv):
import test
if not hasattr(test, "test_function"):# only true if not called via "main"
return pamh.PAM_SUCCESS # normally happens only if run by ctest
test_function = globals()[test.test_function.__name__]
return test_function(test.test_results, who, pamh, flags, argv)
def run_test(caller):
import test
test_name = caller.__name__[4:]
sys.stdout.write("Testing " + test_name + " ")
sys.stdout.flush()
test.test_results = []
test.test_function = globals()["test_" + test_name]
caller(test.test_results)
sys.stdout.write("OK\n")
def pam_conv(auth, query_list, userData=None):
return query_list
#
# Verify the results match.
#
def assert_results(expected_results, results):
for i in range(min(len(expected_results), len(results))):
assert expected_results[i] == results[i], (i, expected_results[i], results[i])
if len(expected_results) < len(results):
assert len(expected_results) == len(results), (i, results[len(expected_results)])
else:
assert len(expected_results) == len(results), (i, expected_results[len(results)])
#
# Test all the calls happen.
#
def test_basic_calls(results, who, pamh, flags, argv):
results.append((py23_function_name(who), flags, argv))
return pamh.PAM_SUCCESS
def run_basic_calls(results):
pam = PAM.pam()
pam.start(TEST_PAM_MODULE, TEST_PAM_USER, pam_conv)
pam.authenticate(0)
pam.acct_mgmt()
pam.chauthtok()
pam.open_session()
pam.close_session()
del pam
me = os.path.join(os.getcwd(), __file__)
expected_results = [
(py23_function_name(pam_sm_authenticate), 0, [me]),
(py23_function_name(pam_sm_acct_mgmt), 0, [me, 'arg1', 'arg2']),
(py23_function_name(pam_sm_chauthtok), 16384, [me]),
(py23_function_name(pam_sm_chauthtok), 8192, [me]),
(py23_function_name(pam_sm_open_session), 0, [me]),
(py23_function_name(pam_sm_close_session), 0, [me]),
(py23_function_name(pam_sm_end), None, None)]
assert_results(expected_results, results)
#
# Test all the constants are defined.
#
PAM_CONSTANTS = {
#
# Constants defined in _pam_types.h. The item constants are omitted.
#
"PAM_SUCCESS": 0,
"PAM_OPEN_ERR": 1,
"PAM_SYMBOL_ERR": 2,
"PAM_SERVICE_ERR": 3,
"PAM_SYSTEM_ERR": 4,
"PAM_BUF_ERR": 5,
"PAM_PERM_DENIED": 6,
"PAM_AUTH_ERR": 7,
"PAM_CRED_INSUFFICIENT": 8,
"PAM_AUTHINFO_UNAVAIL": 9,
"PAM_USER_UNKNOWN": 10,
"PAM_MAXTRIES": 11,
"PAM_NEW_AUTHTOK_REQD": 12,
"PAM_ACCT_EXPIRED": 13,
"PAM_SESSION_ERR": 14,
"PAM_CRED_UNAVAIL": 15,
"PAM_CRED_EXPIRED": 16,
"PAM_CRED_ERR": 17,
"PAM_NO_MODULE_DATA": 18,
"PAM_CONV_ERR": 19,
"PAM_AUTHTOK_ERR": 20,
"PAM_AUTHTOK_RECOVER_ERR": 21,
"PAM_AUTHTOK_RECOVERY_ERR": 21,
"PAM_AUTHTOK_LOCK_BUSY": 22,
"PAM_AUTHTOK_DISABLE_AGING": 23,
"PAM_TRY_AGAIN": 24,
"PAM_IGNORE": 25,
"PAM_ABORT": 26,
"PAM_AUTHTOK_EXPIRED": 27,
"PAM_MODULE_UNKNOWN": 28,
"PAM_BAD_ITEM": 29,
"PAM_CONV_AGAIN": 30,
"PAM_INCOMPLETE": 31,
"PAM_SERVICE": 1,
"PAM_USER": 2,
"PAM_TTY": 3,
"PAM_RHOST": 4,
"PAM_CONV": 5,
"PAM_AUTHTOK": 6,
"PAM_OLDAUTHTOK": 7,
"PAM_RUSER": 8,
"PAM_USER_PROMPT": 9,
"PAM_FAIL_DELAY": 10,
"PAM_XDISPLAY": 11,
"PAM_XAUTHDATA": 12,
"PAM_AUTHTOK_TYPE": 13,
"PAM_SILENT": 0x8000,
"PAM_DISALLOW_NULL_AUTHTOK": 0x0001,
"PAM_ESTABLISH_CRED": 0x0002,
"PAM_DELETE_CRED": 0x0004,
"PAM_REINITIALIZE_CRED": 0x0008,
"PAM_REFRESH_CRED": 0x0010,
"PAM_CHANGE_EXPIRED_AUTHTOK": 0x0020,
"PAM_DATA_SILENT": 0x40000000,
"PAM_PROMPT_ECHO_OFF": 1,
"PAM_PROMPT_ECHO_ON": 2,
"PAM_ERROR_MSG": 3,
"PAM_TEXT_INFO": 4,
"PAM_RADIO_TYPE": 5,
"PAM_BINARY_PROMPT": 7,
"PAM_MAX_NUM_MSG": 32,
"PAM_MAX_MSG_SIZE": 512,
"PAM_MAX_RESP_SIZE": 512,
"_PAM_RETURN_VALUES": 32,
#
# Constants defined in pam_modules.h. The item constants are omitted.
#
"PAM_PRELIM_CHECK": 0x4000,
"PAM_UPDATE_AUTHTOK": 0x2000,
"PAM_DATA_REPLACE": 0x20000000,
}
def test_constants(results, who, pamh, flags, argv):
results.append(py23_function_name(who))
if who != pam_sm_authenticate:
return pamh.PAM_SUCCESS
pam_constants = dict([
(var, getattr(pamh,var))
for var in dir(pamh)
if var.startswith("PAM_") or var.startswith("_PAM_")])
results.append(pam_constants)
try:
pamh.PAM_SUCCESS = 1
results.append("Opps, pamh.PAM_SUCCESS = 1 worked!")
except py23_standard_exception as e:
results.append("except: %s" % e)
return pamh.PAM_SUCCESS
def run_constants(results):
pam = PAM.pam()
pam.start(TEST_PAM_MODULE, TEST_PAM_USER, pam_conv)
pam.authenticate(0)
pam.close_session()
del pam
assert results[0] == py23_function_name(pam_sm_authenticate), (results[0], py23_function_name(pam_sm_authenticate))
assert results[2] == "except: attribute 'PAM_SUCCESS' of 'PamHandle_type' objects is not writable", results[2]
assert results[3] == py23_function_name(pam_sm_close_session), (results[3], py23_function_name(pam_sm_close_session))
assert results[4] == py23_function_name(pam_sm_end), (results[4], py23_function_name(pam_sm_end))
consts = results[1]
for var in PAM_CONSTANTS.keys():
assert var in consts, var
assert consts[var] == PAM_CONSTANTS[var], (var, consts[var], PAM_CONSTANTS[var])
for var in consts.keys():
assert var in PAM_CONSTANTS, var
assert PAM_CONSTANTS[var] == consts[var], (var, PAM_CONSTANTS[var], consts[var])
assert len(results) == 5, len(results)
#
# Test the environment calls.
#
def test_environment(results, who, pamh, flags, argv):
results.append(py23_function_name(who))
if who != pam_sm_acct_mgmt:
return pamh.PAM_SUCCESS
def test_exception(func):
try:
func()
return str(None)
except Exception as e:
return e.__class__.__name__ + ": " + str(e)
#
# A few things to test here. First that PamEnv_as_mapping works.
#
results.append(len(pamh.env))
results.append(pamh.env["x1"])
pamh.env["yy"] = "y"
results.append(pamh.env["yy"])
pamh.env["yy"] = "z"
results.append(pamh.env["yy"])
def t(): pamh.env["yy"] = 1
results.append(test_exception(t))
del pamh.env["yy"]
results.append(test_exception(lambda: pamh.env["yy"]))
results.append(test_exception(lambda: pamh.env[1]))
results.append(test_exception(lambda: pamh.env['a=']))
results.append(test_exception(lambda: pamh.env['']))
#
# Now the dict functions.
#
pamh.env["xx"] = "x"
results.append("not in" in pamh.env)
results.append("xx" in pamh.env)
results.append("not in" in pamh.env)
results.append("xx" in pamh.env)
results.append(test_exception(lambda: pamh.env.__getitem__("not in")))
results.append(pamh.env.get("not in"))
results.append(pamh.env.get("not in", "default"))
results.append(pamh.env.get("xx"))
results.append(pamh.env.get("xx", "default"))
del pamh.env["x1"]
results.append(list(pamh.env.items()))
results.append(list(pamh.env.keys()))
results.append(list(pamh.env.values()))
return pamh.PAM_SUCCESS
def run_environment(results):
pam = PAM.pam()
pam.start(TEST_PAM_MODULE, TEST_PAM_USER, pam_conv)
pam.authenticate(0)
pam.putenv("x1=1")
pam.putenv("x2=2")
pam.putenv("x3=3")
pam.acct_mgmt()
pam.close_session()
del pam
expected_results = [
py23_function_name(pam_sm_authenticate), py23_function_name(pam_sm_acct_mgmt),
3, '1', 'y', 'z',
'TypeError: PAM environment value must be a string',
"KeyError: 'yy'",
'TypeError: PAM environment key must be a string',
"ValueError: PAM environment key can't contain '='",
"ValueError: PAM environment key mustn't be 0 length",
False, True, False, True,
"KeyError: 'not in'",
None, 'default', 'x', 'x',
[('x2', '2'), ('x3', '3'), ('xx', 'x')],
['x2', 'x3', 'xx'],
['2', '3', 'x'],
py23_function_name(pam_sm_close_session), py23_function_name(pam_sm_end)]
assert_results(expected_results, results)
#
# Test strerror().
#
def test_strerror(results, who, pamh, flags, argv):
results.append(py23_function_name(who))
if who != pam_sm_authenticate:
return pamh.PAM_SUCCESS
results.extend([(e, pamh.strerror(e).lower()) for e in (0, 1, 30, 31)])
return pamh.PAM_SUCCESS
def run_strerror(results):
pam = PAM.pam()
pam.start(TEST_PAM_MODULE, TEST_PAM_USER, pam_conv)
pam.authenticate(0)
del pam
expected_results = [
py23_function_name(pam_sm_authenticate),
( 0, 'success'),
( 1, 'failed to load module'),
(30, 'conversation is waiting for event'),
(31, 'application needs to call libpam again'),
py23_function_name(pam_sm_end)]
assert_results(expected_results, results)
#
# Test items.
#
def test_items(results, who, pamh, flags, argv):
results.append(py23_function_name(who))
if not who in (pam_sm_open_session, pam_sm_close_session):
return pamh.PAM_SUCCESS
items = {
"authtok": "authtok-module",
"authtok_type": "authtok_type-module",
"oldauthtok": "oldauthtok-module",
"rhost": "rhost-module",
"ruser": "ruser-module",
"tty": "tty-module",
"user_prompt": "user_prompt-module",
"user": "user-module",
"xdisplay": "xdisplay-module",
}
for key in sorted(items.keys()):
results.append((key, getattr(pamh, key)))
value = items[key]
if value != None:
setattr(pamh, key, value)
try:
setattr(pamh, "tty", 1)
results.append("%r = %r" % (key, value))
except py23_standard_exception as e:
results.append("except: %s" % e)
results.append(pamh.get_user("a prompt"))
return pamh.PAM_SUCCESS
def run_items(results):
pam = PAM.pam()
pam.start(TEST_PAM_MODULE, TEST_PAM_USER, pam_conv)
pam.authenticate(0)
items = {
2: "user",
3: "tty",
4: "rhost",
8: "ruser",
9: "user_prompt",
11: "xdisplay",
13: "authtok_type"}
for item in sorted(items.keys()):
pam.set_item(item, items[item])
pam.open_session()
pam.close_session()
del pam
expected_results = [
py23_function_name(pam_sm_authenticate), py23_function_name(pam_sm_open_session),
('authtok', None),
('authtok_type', 'authtok_type'),
('oldauthtok', None),
('rhost', 'rhost'),
('ruser', 'ruser'),
('tty', 'tty'),
('user', 'user'),
('user_prompt', 'user_prompt'),
('xdisplay', 'xdisplay'),
'except: PAM item PAM_TTY must be set to a string',
'user-module',
py23_function_name(pam_sm_close_session),
('authtok', 'authtok-module'),
('authtok_type', 'authtok_type-module'),
('oldauthtok', 'oldauthtok-module'),
('rhost', 'rhost-module'),
('ruser', 'ruser-module'),
('tty', 'tty-module'),
('user', 'user-module'),
('user_prompt', 'user_prompt-module'),
('xdisplay', 'xdisplay-module'),
'except: PAM item PAM_TTY must be set to a string',
'user-module',
py23_function_name(pam_sm_end)]
assert_results(expected_results, results)
#
# Test the xauthdata item.
#
def test_xauthdata(results, who, pamh, flags, argv):
results.append(py23_function_name(who))
if not who in (pam_sm_open_session, pam_sm_close_session):
return pamh.PAM_SUCCESS
xauthdata0 = pamh.XAuthData("name-module", "data-module")
pamh.xauthdata = xauthdata0
xauthdata1 = pamh.xauthdata
results.append('name=%r, data=%r' % (xauthdata1.name, xauthdata1.data))
try:
xauthdata2 = pamh.XAuthData(None, "x")
results.append('pamh.XAuthData(%r, %r)' % (xauthdata2.name, xauthdata2.data))
except TypeError as e:
results.append('except: %s' % e)
try:
xauthdata2 = pamh.XAuthData("x", 1)
results.append('pamh.XAuthData(%r, %r)' % (xauthdata2.name, xauthdata2.data))
except TypeError as e:
results.append('except: %s' % e)
class XA: pass
XA.name = "name-XA"
XA.data = "data-XA"
pamh.xauthdata = XA
xauthdata2 = pamh.xauthdata
results.append('name=%r, data=%r' % (xauthdata2.name, xauthdata2.data))
xa = XA()
xa.name = "name-xa"
xa.data = "data-xa"
pamh.xauthdata = xa
xauthdata4 = pamh.xauthdata
results.append('name=%r, data=%r' % (xauthdata4.name, xauthdata4.data))
return pamh.PAM_SUCCESS
def run_xauthdata(results):
pam = PAM.pam()
pam.start(TEST_PAM_MODULE, TEST_PAM_USER, pam_conv)
pam.authenticate(0)
#
# The PAM module doesn't support XAUTHDATA, so check what we can from the
# module only.
#
pam.open_session()
pam.close_session()
del pam
expected_results = [
py23_function_name(pam_sm_authenticate), py23_function_name(pam_sm_open_session),
("name='name-module', data='data-module'"),
'except: XAuthData() argument 1 must be string, not None',
'except: XAuthData() argument 2 must be string, not int',
("name='name-XA', data='data-XA'"),
("name='name-xa', data='data-xa'"),
py23_function_name(pam_sm_close_session),
("name='name-module', data='data-module'"),
'except: XAuthData() argument 1 must be string, not None',
'except: XAuthData() argument 2 must be string, not int',
("name='name-XA', data='data-XA'"),
("name='name-xa', data='data-xa'"),
py23_function_name(pam_sm_end)]
assert_results(expected_results, results)
#
# Test having no pam_sm_end.
#
def test_no_sm_end(results, who, pamh, flags, argv):
results.append(py23_function_name(who))
global pam_sm_end
del pam_sm_end
return pamh.PAM_SUCCESS
def run_no_sm_end(results):
pam = PAM.pam()
pam.start(TEST_PAM_MODULE, TEST_PAM_USER, pam_conv)
pam.authenticate(0)
del pam
expected_results = [py23_function_name(pam_sm_authenticate)]
assert_results(expected_results, results)
#
# Test the conversation mechanism.
#
def test_conv(results, who, pamh, flags, argv):
results.append(py23_function_name(who))
if who == pam_sm_end:
return
#
# We must get rid of all references to pamh.Response objects. This instance
# of the test.py module is running inside of libpam_python. That shared
# library will be unloaded soon. Should a pamh.Response instance be
# dealloc'ed after it is unloaded the now non-existant dealloc function will
# be called, and a SIGSEGV will result. Normally instances would not leak,
# but with the trickery we are performing with fake import's here they will
# leak via the results variable unless we take special action.
#
def conv(convs):
responses = pamh.conversation(convs)
if type(responses) != type(()):
return (responses.resp, responses.resp_retcode)
return [(r.resp, r.resp_retcode) for r in responses]
if who == pam_sm_authenticate:
convs = [
pamh.Message(pamh.PAM_PROMPT_ECHO_OFF, "Prompt_echo_off"),
pamh.Message(pamh.PAM_PROMPT_ECHO_ON, "Prompt_echo_on"),
pamh.Message(pamh.PAM_ERROR_MSG, "Error_msg"),
pamh.Message(pamh.PAM_TEXT_INFO, "Text_info")]
if who == pam_sm_acct_mgmt:
convs = pamh.Message(pamh.PAM_PROMPT_ECHO_OFF, "single")
results.append(conv(convs))
return pamh.PAM_SUCCESS
def run_conv(results):
pam = PAM.pam()
pam.start(TEST_PAM_MODULE, TEST_PAM_USER, pam_conv)
pam.authenticate(0)
pam.acct_mgmt()
del pam
expected_results = [
py23_function_name(pam_sm_authenticate),
[('Prompt_echo_off', 1), ('Prompt_echo_on', 2), ('Error_msg', 3), ('Text_info', 4)],
py23_function_name(pam_sm_acct_mgmt),
('single', 1),
py23_function_name(pam_sm_end)]
assert_results(expected_results, results)
#
# Test pam error returns.
#
def test_pamerr(results, who, pamh, flags, argv):
return results[-1]
def run_pamerr(results):
pam = PAM.pam()
pam.start(TEST_PAM_MODULE, TEST_PAM_USER, pam_conv)
for err in range(0, PAM._PAM_RETURN_VALUES):
results.append(err)
try:
pam.authenticate(0)
except PAM.error as e:
results[-1] = -e.args[1]
del pam
expected_results = [-r for r in range(PAM._PAM_RETURN_VALUES)]
expected_results[25] = -6
assert_results(expected_results, results)
#
# Test fail_delay.
#
def test_fail_delay(results, who, pamh, flags, argv):
pamh.fail_delay(10)
return pamh.PAM_SUCCESS
def run_fail_delay(results):
pam = PAM.pam()
pam.start(TEST_PAM_MODULE, TEST_PAM_USER, pam_conv)
pam.authenticate(0)
del pam
#
# Test raising an exception.
#
def test_exceptions(results, who, pamh, flags, argv):
if who != pam_sm_end:
return pamh.PAM_SUCCESS
#
# Here we have use of a backdoor put into pam_python.c specifically
# for testing raising exceptions. Oddly, normally PAM should never
# return anything other than PAM_SUCCESS to anything pam_python.c
# calls.
#
debug_magic = 0x4567abcd
results.append(pamh._PAM_RETURN_VALUES)
for err in range(pamh._PAM_RETURN_VALUES):
try:
pamh.strerror(debug_magic + err)
results.append(err)
except pamh.exception as e:
results.append((-e.pam_result,))
return pamh.PAM_SUCCESS
def run_exceptions(results):
pam = PAM.pam()
pam.start(TEST_PAM_MODULE, TEST_PAM_USER, pam_conv)
pam.authenticate(0)
del pam
expected_results = [results[0], 0]
expected_results += [(-r,) for r in range(1, results[0])]
assert_results(expected_results, results)
#
# Test absent entry point.
#
def test_absent(results, who, pamh, flags, argv):
results.append(py23_function_name(who))
if who != pam_sm_authenticate:
return pamh.PAM_SUCCESS
global pam_sm_acct_mgmt; del pam_sm_acct_mgmt
global pam_sm_setcred; del pam_sm_setcred
global pam_sm_open_session; del pam_sm_open_session
global pam_sm_close_session; del pam_sm_close_session
global pam_sm_chauthtok; del pam_sm_chauthtok
return pamh.PAM_SUCCESS
def run_absent(results):
pam = PAM.pam()
pam.start(TEST_PAM_MODULE, TEST_PAM_USER, pam_conv)
pam.authenticate(0)
funcs = (
pam.acct_mgmt,
pam.setcred,
pam.open_session,
pam.close_session,
pam.chauthtok
)
for func in funcs:
try:
func(0)
exception = None
except py23_base_exception as e:
exception = e
results.append((exception.__class__.__name__, str(exception)))
del pam
expected_results = [
'pam_sm_authenticate',
('error', "('Symbol not found', 2)"),
('error', "('Symbol not found', 2)"),
('error', "('Symbol not found', 2)"),
('error', "('Symbol not found', 2)"),
('error', "('Symbol not found', 2)"),
]
assert_results(expected_results, results)
#
# Entry point.
#
def main(argv):
run_test(run_basic_calls)
run_test(run_constants)
run_test(run_environment)
run_test(run_strerror)
run_test(run_items)
run_test(run_xauthdata)
run_test(run_no_sm_end)
run_test(run_conv)
run_test(run_pamerr)
run_test(run_fail_delay)
run_test(run_exceptions)
run_test(run_absent)
#
# If run from Python run the test suite. Otherwse we are being used
# as a real PAM module presumable from ctest, so just make every call
# return success.
#
if __name__ == "__main__":
import PAM
main(sys.argv)