Compare commits

...

10 Commits

Author SHA1 Message Date
holger krekel
47091db28c renames 2023-10-13 11:02:57 +02:00
holger krekel
f013ff434e rename chatctl to doveauth 2023-10-13 10:43:07 +02:00
holger krekel
4ba5b49d19 rename hpk to c1 inv 2023-10-13 09:36:59 +02:00
holger krekel
15d650dc83 move tox.ini into pyproject.toml 2023-10-13 09:36:27 +02:00
holger krekel
5f5cc99567 first test for python chatctl part and applying black 2023-10-12 19:32:49 +02:00
holger krekel
a1f0854f33 refactor python part a little (thanks missytake) 2023-10-12 19:18:16 +02:00
holger krekel
f2a26bc5fe added more tests 2023-10-12 19:01:47 +02:00
holger krekel
940b39bce7 more tests, better interface between lua and chatctl, also passing dovecot extras 2023-10-12 18:50:40 +02:00
holger krekel
067252703f another test 2023-10-12 17:55:46 +02:00
holger krekel
77d800b13f write a bit of tests for a lua script that calls into chatctl.py (#1)
* add a lua test script running directly against  chatctl.py
2023-10-12 17:41:57 +02:00
11 changed files with 246 additions and 72 deletions

View File

@@ -8,3 +8,23 @@ version = "0.1"
dependencies = [ dependencies = [
"pyinfra", "pyinfra",
] ]
[tool.pytest.ini_options]
addopts = "-v -ra --strict-markers"
[tool.tox]
legacy_tox_ini = """
[tox]
isolated_build = true
envlist = lint
[testenv:lint]
skipdist = True
skip_install = True
deps =
ruff
black
commands =
black --quiet --check --diff src/
ruff src/
"""

View File

@@ -2,7 +2,6 @@
Chat Mail pyinfra deploy. Chat Mail pyinfra deploy.
""" """
import importlib.resources import importlib.resources
from io import StringIO
from pyinfra import host, logger from pyinfra import host, logger
from pyinfra.operations import apt, files, server, systemd, python from pyinfra.operations import apt, files, server, systemd, python
@@ -14,7 +13,7 @@ def _install_chatctl() -> None:
"""Setup chatctl.""" """Setup chatctl."""
files.put( files.put(
src=importlib.resources.files(__package__) src=importlib.resources.files(__package__)
.joinpath("chatctl/chatctl.py") .joinpath("dovecot/doveauth.py")
.open("rb"), .open("rb"),
dest="/home/vmail/chatctl", dest="/home/vmail/chatctl",
user="vmail", user="vmail",
@@ -104,8 +103,8 @@ def _configure_dovecot(mail_server: str) -> bool:
# luarocks install http lpeg_patterns fifo # luarocks install http lpeg_patterns fifo
auth_script = files.put( auth_script = files.put(
src=importlib.resources.files(__package__).joinpath("dovecot/auth.lua"), src=importlib.resources.files(__package__).joinpath("dovecot/doveauth.lua"),
dest="/etc/dovecot/auth.lua", dest="/etc/dovecot/doveauth.lua",
user="root", user="root",
group="root", group="root",
mode="644", mode="644",
@@ -118,7 +117,7 @@ def _configure_dovecot(mail_server: str) -> bool:
def deploy_chatmail(mail_domain: str, mail_server: str, dkim_selector: str) -> None: def deploy_chatmail(mail_domain: str, mail_server: str, dkim_selector: str) -> None:
"""Deploy a chat-mail instance. """Deploy a chat-mail instance.
:param mail_domain: the domain part of your future email addresses, so "example.org" in user@example.org :param mail_domain: domain part of your future email addresses
:param mail_server: the DNS name under which your mail server is reachable :param mail_server: the DNS name under which your mail server is reachable
:param dkim_selector: :param dkim_selector:
""" """

View File

@@ -1,17 +0,0 @@
#!/usr/bin/env python3
import base64
import sys
if sys.argv[1] == "hexauth":
login = base64.b16decode(sys.argv[2])
password = base64.b16decode(sys.argv[3])
if login == b"link2xt@instant2.testrun.org" and password == b"Ahyei6ie":
sys.exit(0)
else:
sys.exit(1)
elif sys.argv[1] == "hexlookup":
login = base64.b16decode(sys.argv[2])
if login == b"link2xt@instant2.testrun.org":
sys.exit(0)
else:
sys.exit(1)

View File

@@ -1,35 +0,0 @@
-- Lua based authentication script for Dovecot.
--
-- It calls external chatctl command to answer requests.
-- Hexadecimal aka base16 encoding.
function hex(data)
return (data:gsub(".", function(char) return string.format("%2X", char:byte()) end))
end
-- Escape shell argument by hex encoding it and wrapping in quotes.
function escape(data)
return ("'"..hex(data).."'")
end
function auth_password_verify(request, password)
if os.execute("/home/vmail/chatctl hexauth "..escape(request.user).." "..escape(password)) then
return dovecot.auth.PASSDB_RESULT_OK, {}
end
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, ""
end
function auth_passdb_lookup(request)
if os.execute("/home/vmail/chatctl hexlookup "..escape(request.user)) then
return dovecot.auth.PASSDB_RESULT_OK, {}
end
return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, "no such user"
end
function auth_userdb_lookup(request)
if os.execute("/home/vmail/chatctl hexlookup "..escape(request.user)) then
return dovecot.auth.USERDB_RESULT_OK, "uid=vmail gid=vmail"
end
return dovecot.auth.USERDB_RESULT_USER_UNKNOWN, "no such user"
end

View File

@@ -0,0 +1,60 @@
-- Escape shell argument by hex encoding it and wrapping in quotes.
function escape(data)
b16 = data:gsub(".", function(char) return string.format("%2X", char:byte()) end)
return ("'"..b16.."'")
end
-- call out to python program to actually manage authentication for dovecot
function chatctl_verify(user, password)
local handle = io.popen("python doveauth.py hexauth "..escape(user).." "..escape(password))
local result = handle:read("*a")
handle:close()
return split_chatctl(result)
end
function chatctl_lookup(user)
assert(user)
local handle = io.popen("python doveauth.py hexlookup "..escape(user))
local result = handle:read("*a")
handle:close()
return split_chatctl(result)
end
function get_extra_dovecot_output(res)
return {homedir=res.homedir, uid=res.uid, gid=res.gid}
end
function auth_passdb_verify(request, password)
local res = chatctl_verify(request.user, password)
if res.status == "ok" then
return dovecot.auth.PASSDB_RESULT_OK, get_extra_dovecot_output(res)
end
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, ""
end
function auth_passdb_lookup(request)
local res = chatctl_lookup(request.user)
if res.status == "ok" then
return dovecot.auth.PASSDB_RESULT_OK, get_extra_dovecot_output(res)
end
return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, "no such user"
end
function auth_userdb_lookup(request)
local res = chatctl_lookup(request.user)
if res.status == "ok" then
return dovecot.auth.USERDB_RESULT_OK, get_extra_dovecot_output(res)
end
return dovecot.auth.USERDB_RESULT_USER_UNKNOWN, "no such user"
end
function split_chatctl(output)
local ret = {}
for key, value in output:gmatch "(%w+)%s*=%s*(%w+)" do
ret[key] = value
end
return ret
end

View File

@@ -0,0 +1,61 @@
#!/usr/bin/env python3
import base64
import sys
def get_user_data(user):
if user == b"link2xt@instant2.testrun.org":
return dict(
homedir="/home/vmail/link2xt",
uid="vmail",
gid="vmail",
password=b"Ahyei6ie",
)
return {}
def create_user(user, password):
assert isinstance(password, bytes)
return dict(
homedir=f"/home/vmail/{user}", uid="vmail", gid="vmail", password=password
)
def verify_user(user, password):
userdata = get_user_data(user)
if userdata:
if userdata.get("password") == password:
userdata["status"] = "ok"
else:
userdata["status"] = "fail"
else:
userdata = create_user(user, password)
userdata["status"] = "ok"
return userdata
def lookup_user(user):
userdata = get_user_data(user)
if userdata:
userdata["status"] = "ok"
else:
userdata["status"] = "fail"
return userdata
def dump_result(res):
for key, value in res.items():
print(f"{key}={value}")
if __name__ == "__main__":
if sys.argv[1] == "hexauth":
login = base64.b16decode(sys.argv[2])
password = base64.b16decode(sys.argv[3])
res = verify_user(login, password)
dump_result(res)
elif sys.argv[1] == "hexlookup":
login = base64.b16decode(sys.argv[2])
res = lookup_user(login)
dump_result(res)

View File

@@ -7,11 +7,11 @@ auth_mechanisms = plain
# Authentication for system users. # Authentication for system users.
passdb { passdb {
driver = lua driver = lua
args = file=/etc/dovecot/auth.lua args = file=/etc/dovecot/doveauth.lua
} }
userdb { userdb {
driver = lua driver = lua
args = file=/etc/dovecot/auth.lua args = file=/etc/dovecot/doveauth.lua
} }
## ##

View File

@@ -0,0 +1,76 @@
require "doveauth"
-- simulate dovecot defined result codes
dovecot = {
auth = {
PASSDB_RESULT_OK="PASSWORD-OK",
PASSDB_RESULT_PASSWORD_MISMATCH="PASSWORD-MISMATCH",
USERDB_RESULT_OK="USERDB-OK",
USERDB_RESULT_USER_UNKNOWN="USERDB-UNKNOWN"
}
}
-- Tests for testing the lua<->python interaction
function test_passdb_verify_ok(user, password)
local res, extra = auth_passdb_verify({user=user}, password)
assert(res==dovecot.auth.PASSDB_RESULT_OK)
assert(extra.uid == "vmail")
assert(extra.gid == "vmail")
-- assert(extra.homedir == "/home/vmail/link2xt")
print("OK test_passdb_verify_ok "..user.." "..password)
end
function test_passdb_verify_mismatch(user, password)
local res = auth_passdb_verify({user=user}, password)
assert(res == dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH)
print("OK test_passdb_verify_mismatch "..user.." "..password)
end
function test_userdb_lookup_ok(user)
local res, extra = auth_userdb_lookup({user=user})
assert(extra.uid == "vmail")
assert(extra.gid == "vmail")
assert(res == dovecot.auth.USERDB_RESULT_OK)
print("OK test_userdb_lookup_ok "..user)
end
function test_userdb_lookup_mismatch(user)
local res, extra = auth_userdb_lookup({user=user})
assert(res == dovecot.auth.USERDB_RESULT_USER_UNKNOWN)
print("OK test_userdb_lookup_mismatch "..user)
end
function test_passdb_lookup_ok(user)
local res, extra = auth_passdb_lookup({user=user})
assert(extra.uid == "vmail")
assert(extra.gid == "vmail")
assert(res == dovecot.auth.PASSDB_RESULT_OK)
print("OK test_passdb_lookup_ok "..user)
end
function test_passdb_lookup_mismatch(user)
local res, extra = auth_passdb_lookup({user=user})
assert(res == dovecot.auth.PASSDB_RESULT_USER_UNKNOWN)
print("OK test_passdb_lookup_mismatch "..user)
end
function test_split_chatctl()
local res = split_chatctl("a=3 b=4\nc=5")
assert(res["a"] == "3")
assert(res["b"] == "4")
assert(res["c"] == "5")
print("OK test_split_chatctl")
end
test_split_chatctl()
test_passdb_verify_ok("link2xt@instant2.testrun.org", "Ahyei6ie")
test_passdb_verify_mismatch("link2xt@instant2.testrun.org", "Aqwlek")
test_userdb_lookup_ok("link2xt@instant2.testrun.org")
test_userdb_lookup_mismatch("wlekqjlew@xyz.org")
test_passdb_lookup_ok("link2xt@instant2.testrun.org")
test_passdb_lookup_mismatch("llqkwjelqwe@xyz.org")

View File

@@ -0,0 +1,23 @@
import subprocess
import pytest
from doveauth import get_user_data, verify_user
def test_basic():
data = get_user_data(b"link2xt@instant2.testrun.org")
assert data
@pytest.mark.xfail(reason="no persistence yet")
def test_verify_or_create():
res = verify_user(b"newuser1@something.org", b"kajdlkajsldk12l3kj1983")
assert res["status"] == "ok"
res = verify_user(b"newuser1@something.org", b"kajdlqweqwe")
assert res["status"] == "fail"
def test_lua_integration(request):
p = request.fspath.dirpath("test_doveauth.lua")
proc = subprocess.run(["lua", str(p)])
assert proc.returncode == 0

13
tox.ini
View File

@@ -1,13 +0,0 @@
[tox]
isolated_build = true
envlist = lint
[testenv:lint]
skipdist = True
skip_install = True
deps =
ruff
black
commands =
black --quiet --check --diff src/
ruff src/