Compare commits

..

19 Commits

Author SHA1 Message Date
link2xt
0f89b52d5b Add init.sh and deploy.sh scripts 2023-10-13 14:11:13 +00:00
link2xt
9da375cf5c README: add --ssh-user root 2023-10-13 14:06:30 +00:00
missytake
8086e2ee2f removed inventory, doesn't work anymore anyway 2023-10-13 14:28:08 +02:00
holger krekel
1b88e0d9d0 add a functional online test for login ok/failure 2023-10-13 14:23:14 +02:00
holger krekel
db6df34703 python tests work 2023-10-13 14:23:11 +02:00
holger krekel
a907da9907 wip 2023-10-13 14:22:08 +02:00
holger krekel
14649c37fd this seems to work 2023-10-13 14:22:08 +02:00
holger krekel
28fe373489 (nami, hpk) some random WIP-changes to make dovecot auth work and get us an understanding 2023-10-13 14:22:08 +02:00
missytake
1942ad3cef ensure environment variables are set 2023-10-13 11:17:55 +02:00
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
17 changed files with 287 additions and 158 deletions

View File

@@ -16,5 +16,5 @@ For DKIM you must add a DNS entry as in /etc/opendkim/selector.txt (where select
## Run with pyinfra ## Run with pyinfra
``` ```
CHATMAIL_DOMAIN=c1.testrun.org pyinfra c1.testrun.org deploy.py CHATMAIL_DOMAIN=c1.testrun.org pyinfra --ssh-user root c1.testrun.org deploy.py
``` ```

View File

@@ -8,7 +8,11 @@ def main():
mail_server = os.getenv("CHATMAIL_SERVER", mail_domain) mail_server = os.getenv("CHATMAIL_SERVER", mail_domain)
dkim_selector = os.getenv("CHATMAIL_DKIM_SELECTOR", "2023") dkim_selector = os.getenv("CHATMAIL_DKIM_SELECTOR", "2023")
assert mail_domain
assert mail_server
assert dkim_selector
deploy_chatmail(mail_domain, mail_server, dkim_selector) deploy_chatmail(mail_domain, mail_server, dkim_selector)
main() main()

View File

@@ -1,11 +0,0 @@
chatmail = [
(
"c1.testrun.org",
{
"ssh_user": "root",
"domain": "c1.testrun.org",
"dkim_selector": "2023",
},
),
]

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/
"""

3
scripts/deploy.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/env bash
export CHATMAIL_DOMAIN="${1:-c1.testrun.org}"
venv/bin/pyinfra --ssh-user root "$CHATMAIL_DOMAIN" deploy.py

4
scripts/init.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/sh
python3 -m venv venv
venv/bin/pip install pyinfra
venv/bin/pip install -e .

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,73 +0,0 @@
-- To run this test: run "lua test.lua" while in the same directory as chatctl.py
if dovecot == nil then
dovecot = {
auth = {
PASSDB_RESULT_OK="OK",
PASSDB_RESULT_PASSWORD_MISMATCH="MISMATCH"
}
}
end
-- 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)
return os.execute("python chatctl.py hexauth "..escape(user).." "..escape(password))
end
function chatctl_lookup(hex, user)
return os.execute("python chatctl.py hexlookup "..escape(user))
end
function auth_password_verify(request, password)
if chatctl_verify(request.user, password) then
return dovecot.auth.PASSDB_RESULT_OK, {}
end
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, ""
end
function auth_passdb_lookup(request)
if chatctl_lookup(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 chatctl_lookup(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
function split_chatctl_results(output)
local ret = {}
for key, value in output:gmatch "(%w+)%s*=%s*(%w+)" do
ret[key] = value
end
return ret
end
function test_ok(user, password)
local res = auth_password_verify({user=user}, password)
assert(res=="OK")
print("OK test_ok "..user.." "..password)
end
function test_mismatch(user, password)
local res = auth_password_verify({user=user}, password)
assert(res == "MISMATCH")
print("OK test_mismatch "..user.." "..password)
end
test_ok("link2xt@instant2.testrun.org", "Ahyei6ie")
test_mismatch("link2xt@instant2.testrun.org", "Aqwlek")

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,57 @@
-- 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 cmd = "python3 /home/vmail/chatctl hexauth "..escape(user).." "..escape(password)
print("executing: "..cmd)
local handle = io.popen(cmd)
local result = handle:read("*a")
handle:close()
return split_chatctl(result)
end
function chatctl_lookup(user)
assert(user)
local handle = io.popen("python3 /home/vmail/chatctl hexlookup "..escape(user))
local result = handle:read("*a")
handle:close()
return split_chatctl(result)
end
function get_extra_dovecot_output(res)
return {home=res.home, uid=res.uid, gid=res.gid}
end
function auth_password_verify(request, password)
local res = chatctl_verify(request.user, password)
-- request:log_error("auth_password_verify "..request.user.." "..password)
if res.status == "ok" then
local extra = get_extra_dovecot_output(res)
return dovecot.auth.PASSDB_RESULT_OK, get_extra_dovecot_output(res)
end
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, ""
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,57 @@
#!/usr/bin/env python3
import base64
import sys
def get_user_data(user):
if user == "link2xt@c1.testrun.org":
return dict(
uid="vmail",
gid="vmail",
password="Ahyei6ie",
)
return {}
def create_user(user, password):
return dict(home=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]).decode()
password = base64.b16decode(sys.argv[3]).decode()
res = verify_user(login, password)
dump_result(res)
elif sys.argv[1] == "hexlookup":
login = base64.b16decode(sys.argv[2]).decode()
res = lookup_user(login)
dump_result(res)

View File

@@ -4,14 +4,19 @@ protocols = imap lmtp
auth_mechanisms = plain auth_mechanisms = plain
auth_verbose = yes
auth_debug = yes
auth_debug_passwords = yes
auth_verbose_passwords = 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,78 @@
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_password_verify_ok(user, password)
local res, extra = auth_password_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_password_verify_ok "..user.." "..password)
end
function test_password_verify_mismatch(user, password)
local res = auth_password_verify({user=user}, password)
assert(res == dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH)
print("OK test_password_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_password_verify_ok("link2xt@c1.testrun.org", "Ahyei6ie")
test_password_verify_mismatch("link2xt@c1.testrun.org", "Aqwlek")
test_userdb_lookup_ok("link2xt@c1.testrun.org")
test_userdb_lookup_mismatch("wlekqjlew@xyz.org")
-- probably not needed by dovecot?
-- test_passdb_lookup_ok("link2xt@c1.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("link2xt@c1.testrun.org")
assert data
@pytest.mark.xfail(reason="no persistence yet")
def test_verify_or_create():
res = verify_user("newuser1@something.org", "kajdlkajsldk12l3kj1983")
assert res["status"] == "ok"
res = verify_user("newuser1@something.org", "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

View File

@@ -0,0 +1,28 @@
import pytest
import imaplib
@pytest.fixture
def conn():
return connect("c1.testrun.org")
def login(conn, user, password):
print("trying to login", user, password)
conn.login(user, password)
def connect(host):
print(f"connecting to {host}")
conn = imaplib.IMAP4_SSL(host)
return conn
def test_login_ok(conn):
login(conn, "link2xt@c1.testrun.org", "Ahyei6ie")
def test_login_fail(conn):
with pytest.raises(imaplib.IMAP4.error) as excinfo:
login(conn, "link2xt@c1.testrun.org", "qweqwe")
assert "AUTHENTICATIONFAILED" in str(excinfo)

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/