mirror of
https://github.com/chatmail/relay.git
synced 2026-05-19 20:38:05 +00:00
doveauth: switch from lua authentication to dict authentication
Co-Authored-By: holger krekel <holger@merlinux.eu> Co-Authored-By: link2xt <link2xt@testrun.org>
This commit is contained in:
@@ -32,7 +32,6 @@ doveauth
|
|||||||
README.md
|
README.md
|
||||||
pyproject.toml
|
pyproject.toml
|
||||||
doveauth.py
|
doveauth.py
|
||||||
doveauth.lua
|
|
||||||
test_doveauth.py
|
test_doveauth.py
|
||||||
|
|
||||||
# lmtp server to block (outgoing) unencrypted messages
|
# lmtp server to block (outgoing) unencrypted messages
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from .acmetool import deploy_acmetool
|
|||||||
|
|
||||||
def _install_doveauth() -> None:
|
def _install_doveauth() -> None:
|
||||||
"""Setup chatctl."""
|
"""Setup chatctl."""
|
||||||
doveauth_filename = "doveauth-0.1.tar.gz"
|
doveauth_filename = "doveauth-0.2.tar.gz"
|
||||||
doveauth_path = importlib.resources.files(__package__).joinpath(
|
doveauth_path = importlib.resources.files(__package__).joinpath(
|
||||||
f"../../../dist/{doveauth_filename}"
|
f"../../../dist/{doveauth_filename}"
|
||||||
)
|
)
|
||||||
@@ -30,6 +30,24 @@ def _install_doveauth() -> None:
|
|||||||
commands=[f"pip install --break-system-packages {remote_path}"],
|
commands=[f"pip install --break-system-packages {remote_path}"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
files.put(
|
||||||
|
src=importlib.resources.files("doveauth")
|
||||||
|
.joinpath("doveauth-dictproxy.service")
|
||||||
|
.open("rb"),
|
||||||
|
dest="/etc/systemd/system/doveauth-dictproxy.service",
|
||||||
|
user="root",
|
||||||
|
group="root",
|
||||||
|
mode="644",
|
||||||
|
)
|
||||||
|
systemd.service(
|
||||||
|
name="Setup doveauth-dictproxy service",
|
||||||
|
service="doveauth-dictproxy.service",
|
||||||
|
running=True,
|
||||||
|
enabled=True,
|
||||||
|
restarted=True,
|
||||||
|
daemon_reload=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _install_filtermail() -> None:
|
def _install_filtermail() -> None:
|
||||||
"""Setup filtermail."""
|
"""Setup filtermail."""
|
||||||
@@ -152,16 +170,14 @@ def _configure_dovecot(mail_server: str) -> bool:
|
|||||||
config={"hostname": mail_server},
|
config={"hostname": mail_server},
|
||||||
)
|
)
|
||||||
need_restart |= main_config.changed
|
need_restart |= main_config.changed
|
||||||
|
auth_config = files.put(
|
||||||
# luarocks install http lpeg_patterns fifo
|
src=importlib.resources.files(__package__).joinpath("dovecot/auth.conf"),
|
||||||
auth_script = files.put(
|
dest="/etc/dovecot/auth.conf",
|
||||||
src=importlib.resources.files("doveauth").joinpath("doveauth.lua"),
|
|
||||||
dest="/etc/dovecot/doveauth.lua",
|
|
||||||
user="root",
|
user="root",
|
||||||
group="root",
|
group="root",
|
||||||
mode="644",
|
mode="644",
|
||||||
)
|
)
|
||||||
need_restart |= auth_script.changed
|
need_restart |= auth_config.changed
|
||||||
|
|
||||||
return need_restart
|
return need_restart
|
||||||
|
|
||||||
@@ -196,11 +212,7 @@ def deploy_chatmail(mail_domain: str, mail_server: str, dkim_selector: str) -> N
|
|||||||
|
|
||||||
apt.packages(
|
apt.packages(
|
||||||
name="Install Dovecot",
|
name="Install Dovecot",
|
||||||
packages=[
|
packages=["dovecot-imapd", "dovecot-lmtpd"],
|
||||||
"dovecot-imapd",
|
|
||||||
"dovecot-lmtpd",
|
|
||||||
"dovecot-auth-lua",
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
apt.packages(
|
apt.packages(
|
||||||
|
|||||||
5
chatmail-pyinfra/src/chatmail/dovecot/auth.conf
Normal file
5
chatmail-pyinfra/src/chatmail/dovecot/auth.conf
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
uri = proxy:/run/dovecot/doveauth.socket:auth
|
||||||
|
iterate_disable = yes
|
||||||
|
default_pass_scheme = plain
|
||||||
|
password_key = passdb/%w
|
||||||
|
user_key = userdb/%u
|
||||||
@@ -11,14 +11,13 @@ auth_verbose_passwords = plain
|
|||||||
|
|
||||||
# Authentication for system users.
|
# Authentication for system users.
|
||||||
passdb {
|
passdb {
|
||||||
driver = lua
|
driver = dict
|
||||||
args = file=/etc/dovecot/doveauth.lua
|
args = /etc/dovecot/auth.conf
|
||||||
}
|
}
|
||||||
userdb {
|
userdb {
|
||||||
driver = lua
|
driver = dict
|
||||||
args = file=/etc/dovecot/doveauth.lua
|
args = /etc/dovecot/auth.conf
|
||||||
}
|
}
|
||||||
|
|
||||||
##
|
##
|
||||||
## Mailbox locations and namespaces
|
## Mailbox locations and namespaces
|
||||||
##
|
##
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "doveauth"
|
name = "doveauth"
|
||||||
version = "0.1"
|
version = "0.2"
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
doveauth = "doveauth.doveauth:main"
|
doveauth = "doveauth.doveauth:main"
|
||||||
|
doveauth-dictproxy = "doveauth.dictproxy:main"
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
addopts = "-v -ra --strict-markers"
|
addopts = "-v -ra --strict-markers"
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
from .doveauth import get_user_data, verify_user
|
|
||||||
from .database import DBError, Database
|
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ class Database:
|
|||||||
with self.write_transaction() as conn:
|
with self.write_transaction() as conn:
|
||||||
if self.get_schema_version() > 1:
|
if self.get_schema_version() > 1:
|
||||||
raise DBError(
|
raise DBError(
|
||||||
"Database version is %s; downgrading database schema is not supported"
|
"version is %s; downgrading schema is not supported"
|
||||||
% (self.get_schema_version(),)
|
% (self.get_schema_version(),)
|
||||||
)
|
)
|
||||||
conn.execute(
|
conn.execute(
|
||||||
|
|||||||
102
doveauth/src/doveauth/dictproxy.py
Normal file
102
doveauth/src/doveauth/dictproxy.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
from socketserver import (
|
||||||
|
UnixStreamServer,
|
||||||
|
StreamRequestHandler,
|
||||||
|
ThreadingMixIn,
|
||||||
|
)
|
||||||
|
import pwd
|
||||||
|
|
||||||
|
from .database import Database
|
||||||
|
|
||||||
|
|
||||||
|
def create_user(db, user, password):
|
||||||
|
with db.write_transaction() as conn:
|
||||||
|
conn.create_user(user, password)
|
||||||
|
return dict(home=f"/home/vmail/{user}", uid="vmail", gid="vmail", password=password)
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_data(db, user):
|
||||||
|
with db.read_connection() as conn:
|
||||||
|
result = conn.get_user(user)
|
||||||
|
if result:
|
||||||
|
result["uid"] = "vmail"
|
||||||
|
result["gid"] = "vmail"
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def lookup_userdb(db, user):
|
||||||
|
return get_user_data(db, user)
|
||||||
|
|
||||||
|
|
||||||
|
def lookup_passdb(db, user, password):
|
||||||
|
userdata = get_user_data(db, user)
|
||||||
|
if not userdata:
|
||||||
|
return create_user(db, user, password)
|
||||||
|
if userdata.get("password") == password:
|
||||||
|
return userdata
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def handle_dovecot_request(msg, db):
|
||||||
|
print(f"received msg: {msg!r}")
|
||||||
|
short_command = msg[0]
|
||||||
|
if short_command == "L": # LOOKUP
|
||||||
|
parts = msg[1:].split("\t")
|
||||||
|
keyname, user = parts[:2]
|
||||||
|
namespace, type, arg = keyname.split("/", 3)
|
||||||
|
reply_command = "F"
|
||||||
|
res = ""
|
||||||
|
if namespace == "shared":
|
||||||
|
if type == "userdb":
|
||||||
|
res = lookup_userdb(db, user)
|
||||||
|
if res:
|
||||||
|
reply_command = "O"
|
||||||
|
else:
|
||||||
|
reply_command = "N"
|
||||||
|
elif type == "passdb":
|
||||||
|
res = lookup_passdb(db, user, password=arg)
|
||||||
|
if res:
|
||||||
|
reply_command = "O"
|
||||||
|
else:
|
||||||
|
reply_command = "N"
|
||||||
|
print(f"res: {res!r}")
|
||||||
|
json_res = json.dumps(res) if res else ""
|
||||||
|
return f"{reply_command}{json_res}\n"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class ThreadedUnixStreamServer(ThreadingMixIn, UnixStreamServer):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
socket = sys.argv[1]
|
||||||
|
passwd_entry = pwd.getpwnam(sys.argv[2])
|
||||||
|
db = Database(sys.argv[3])
|
||||||
|
|
||||||
|
class Handler(StreamRequestHandler):
|
||||||
|
def handle(self):
|
||||||
|
while True:
|
||||||
|
msg = self.rfile.readline().strip().decode()
|
||||||
|
if not msg:
|
||||||
|
continue
|
||||||
|
res = handle_dovecot_request(msg, db)
|
||||||
|
if res:
|
||||||
|
print(f"sending result: {res!r}")
|
||||||
|
self.wfile.write(res.encode("ascii"))
|
||||||
|
self.wfile.flush()
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.unlink(socket)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
with ThreadedUnixStreamServer(socket, Handler) as server:
|
||||||
|
os.chown(socket, uid=passwd_entry.pw_uid, gid=passwd_entry.pw_gid)
|
||||||
|
try:
|
||||||
|
server.serve_forever()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
10
doveauth/src/doveauth/doveauth-dictproxy.service
Normal file
10
doveauth/src/doveauth/doveauth-dictproxy.service
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Dict authentication proxy for dovecot
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/local/bin/doveauth-dictproxy /run/dovecot/doveauth.socket vmail /home/vmail/passdb.sqlite
|
||||||
|
Restart=always
|
||||||
|
RestartSec=30
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
|
|
||||||
-- 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 = "doveauth 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)
|
|
||||||
local cmd = "doveauth hexlookup "..escape(user)
|
|
||||||
assert(user)
|
|
||||||
print("executing: "..cmd)
|
|
||||||
local handle = io.popen(cmd)
|
|
||||||
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
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
|
|
||||||
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")
|
|
||||||
|
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from doveauth import get_user_data, verify_user, Database, DBError
|
from .dictproxy import get_user_data
|
||||||
|
from .doveauth import verify_user
|
||||||
|
from .database import Database, DBError
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
@@ -24,12 +26,6 @@ def test_verify_or_create(db):
|
|||||||
assert res["status"] == "fail"
|
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
|
|
||||||
|
|
||||||
|
|
||||||
def test_db_version(db):
|
def test_db_version(db):
|
||||||
assert db.get_schema_version() == 1
|
assert db.get_schema_version() == 1
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
pushd doveauth/src/doveauth
|
pushd doveauth/src/doveauth
|
||||||
../../venv/bin/pytest
|
../../venv/bin/pytest
|
||||||
popd
|
popd
|
||||||
|
|||||||
Reference in New Issue
Block a user