Fix setup authentication and scanner detection
This commit is contained in:
+1
-1
@@ -311,7 +311,7 @@ int ConnReadToConnUntilEOS(Connection *Src, Connection *Dest);
|
||||
|
||||
int ConnWrite(Connection *Conn, const void *Source, int Count);
|
||||
int ConnWriteF(Connection *Conn, const char *Format, ...) XPL_PRINTF(2, 3);
|
||||
int ConnWriteVF(Connection *c, const char *format, va_list ap);
|
||||
int ConnWriteVF(Connection *c, const char *format, va_list ap) XPL_PRINTF(2, 0);
|
||||
int ConnWriteFile(Connection *Conn, FILE *Source);
|
||||
int ConnWriteFromFile(Connection *Conn, FILE *Source, int Count);
|
||||
#define ConnWriteStr(conn, mesg) ConnWrite(conn, mesg, strlen(mesg))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import logging, os, pwd, sys
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import logging, os, pwd, sys
|
||||
import getpass
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!@Python3_EXECUTABLE@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# /****************************************************************************
|
||||
# * <Novell-copyright>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!@Python3_EXECUTABLE@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# /****************************************************************************
|
||||
# * <Novell-copyright>
|
||||
|
||||
@@ -134,8 +134,20 @@ class ConfigurationStorageTest(unittest.TestCase):
|
||||
|
||||
def test_scanner_probe_fails_closed_on_protocol_mismatch(self):
|
||||
with mock.patch("socket.create_connection",
|
||||
return_value=FakeSocket(b"unexpected\n")):
|
||||
self.assertFalse(probe_clamav().available)
|
||||
return_value=FakeSocket(b"unexpected\n")), \
|
||||
mock.patch("shutil.which", return_value="/usr/bin/clamd"):
|
||||
result = probe_clamav()
|
||||
self.assertFalse(result.available)
|
||||
self.assertTrue(result.installed)
|
||||
|
||||
def test_scanner_probe_reports_installed_but_unreachable(self):
|
||||
with mock.patch("socket.create_connection",
|
||||
side_effect=ConnectionRefusedError("refused")), \
|
||||
mock.patch("shutil.which", return_value="/usr/bin/spamd"):
|
||||
result = probe_spamd()
|
||||
self.assertFalse(result.available)
|
||||
self.assertTrue(result.installed)
|
||||
self.assertEqual(result.endpoint, "127.0.0.1:783")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# /****************************************************************************
|
||||
# * <Novell-copyright>
|
||||
# * Copyright (c) 2001 Novell, Inc. All Rights Reserved.
|
||||
# *
|
||||
# * This program is free software; you can redistribute it and/or
|
||||
# * modify it under the terms of version 2 of the GNU General Public License
|
||||
# * as published by the Free Software Foundation.
|
||||
# *
|
||||
# * This program is distributed in the hope that it will be useful,
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# * GNU General Public License for more details.
|
||||
# *
|
||||
# * You should have received a copy of the GNU General Public License
|
||||
# * along with this program; if not, contact Novell, Inc.
|
||||
# *
|
||||
# * To contact Novell about this file by physical or electronic mail, you
|
||||
# * may find current contact information at www.novell.com.
|
||||
# * </Novell-copyright>
|
||||
# ****************************************************************************/
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
SOURCE = Path(__file__).resolve().parents[4]
|
||||
sys.path.insert(0, str(SOURCE / "src" / "libs" / "python"))
|
||||
|
||||
from bongo.configuration.probes import ProbeResult # noqa: E402
|
||||
from bongo.configuration.tui import ( # noqa: E402
|
||||
_choice_layout, _scanner_status,
|
||||
)
|
||||
|
||||
|
||||
class ConfigurationTuiTest(unittest.TestCase):
|
||||
def test_single_line_prompt_preserves_original_choice_row(self):
|
||||
self.assertEqual(_choice_layout(24, "Question?", 2, 0), (5, 0, 17))
|
||||
|
||||
def test_multiline_summary_places_choices_after_prompt(self):
|
||||
prompt = "Host: mail.example.test\nDomains: example.test\n\nInstall now?"
|
||||
choice_row, first, visible = _choice_layout(24, prompt, 2, 0)
|
||||
self.assertEqual(choice_row, 8)
|
||||
self.assertEqual(first, 0)
|
||||
self.assertGreaterEqual(visible, 2)
|
||||
|
||||
def test_small_terminal_clamps_choice_rows_and_scrolls(self):
|
||||
prompt = "\n".join(f"summary {number}" for number in range(20))
|
||||
choice_row, first, visible = _choice_layout(12, prompt, 8, 7)
|
||||
self.assertEqual(choice_row, 9)
|
||||
self.assertEqual(visible, 1)
|
||||
self.assertEqual(first, 7)
|
||||
|
||||
def test_scanner_status_distinguishes_unavailable_service(self):
|
||||
result = ProbeResult(False, "127.0.0.1:3310", "refused", True)
|
||||
self.assertEqual(_scanner_status(False, result),
|
||||
"disabled (service unavailable)")
|
||||
self.assertEqual(_scanner_status(True, result),
|
||||
"configured (currently unreachable)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import logging, os, pwd, sys
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import logging, os, pwd, sys
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ add_library(authsqlite3 MODULE
|
||||
target_link_libraries(authsqlite3 PRIVATE
|
||||
bongoauthstore
|
||||
bongoauthsecurity
|
||||
bongomsgapi
|
||||
bongoutil
|
||||
bongoxpl
|
||||
LibGcrypt::LibGcrypt
|
||||
OpenLDAP::LDAP)
|
||||
@@ -55,4 +57,9 @@ if(BUILD_TESTING)
|
||||
${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(auth-ldap-test PRIVATE OpenLDAP::LDAP)
|
||||
add_test(NAME auth-ldap COMMAND auth-ldap-test)
|
||||
|
||||
add_executable(auth-module-load-test tests/module-load-test.c)
|
||||
target_link_libraries(auth-module-load-test PRIVATE ${CMAKE_DL_LIBS})
|
||||
add_test(NAME auth-module-load
|
||||
COMMAND auth-module-load-test $<TARGET_FILE:authsqlite3>)
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/****************************************************************************
|
||||
* <Novell-copyright>
|
||||
* Copyright (c) 2001 Novell, Inc. All Rights Reserved.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of version 2 of the GNU General Public License
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, contact Novell, Inc.
|
||||
*
|
||||
* To contact Novell about this file by physical or electronic mail, you
|
||||
* may find current contact information at www.novell.com.
|
||||
* </Novell-copyright>
|
||||
****************************************************************************/
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <stdio.h>
|
||||
|
||||
int
|
||||
main(int argc, char **argv)
|
||||
{
|
||||
void *module;
|
||||
const char *error;
|
||||
|
||||
if (argc != 2) {
|
||||
fprintf(stderr, "usage: %s AUTH_MODULE\n", argv[0]);
|
||||
return 2;
|
||||
}
|
||||
|
||||
dlerror();
|
||||
module = dlopen(argv[1], RTLD_NOW | RTLD_GLOBAL);
|
||||
if (!module) {
|
||||
error = dlerror();
|
||||
fprintf(stderr, "unable to load %s: %s\n", argv[1],
|
||||
error ? error : "unknown dynamic loader error");
|
||||
return 1;
|
||||
}
|
||||
|
||||
dlclose(module);
|
||||
return 0;
|
||||
}
|
||||
@@ -26,11 +26,15 @@ MsgAuthLoadBackend(const char *name, const char *file)
|
||||
AuthPlugin_InterfaceVersion *version_func;
|
||||
AuthPlugin_Init *init_func;
|
||||
MsgAuthAPIFunction *func;
|
||||
const char *load_error;
|
||||
|
||||
snprintf(path, XPL_MAX_PATH, "%s/bongo-auth/%s", XPL_DEFAULT_LIB_DIR, file);
|
||||
dlerror();
|
||||
auth = XplLoadDLL(path);
|
||||
if (! auth) {
|
||||
Log(LOG_FATAL, "Unable to load auth backend '%s'", file);
|
||||
load_error = dlerror();
|
||||
Log(LOG_FATAL, "Unable to load auth backend '%s': %s",
|
||||
path, load_error ? load_error : "unknown dynamic loader error");
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import socket
|
||||
from dataclasses import dataclass
|
||||
|
||||
@@ -32,6 +33,7 @@ class ProbeResult:
|
||||
available: bool
|
||||
endpoint: str
|
||||
detail: str
|
||||
installed: bool = False
|
||||
|
||||
|
||||
def _receive(connection: socket.socket, maximum: int = 4096) -> bytes:
|
||||
@@ -49,31 +51,33 @@ def _receive(connection: socket.socket, maximum: int = 4096) -> bytes:
|
||||
def probe_clamav(host: str = "127.0.0.1", port: int = 3310,
|
||||
timeout: float = 1.5) -> ProbeResult:
|
||||
endpoint = f"{host}:{port}"
|
||||
installed = shutil.which("clamd") is not None
|
||||
try:
|
||||
with socket.create_connection((host, port), timeout=timeout) as connection:
|
||||
connection.settimeout(timeout)
|
||||
connection.sendall(b"zPING\0")
|
||||
response = _receive(connection)
|
||||
except OSError as error:
|
||||
return ProbeResult(False, endpoint, str(error))
|
||||
return ProbeResult(False, endpoint, str(error), installed)
|
||||
if response.rstrip(b"\0\r\n") == b"PONG":
|
||||
return ProbeResult(True, endpoint, "clamd replied PONG")
|
||||
return ProbeResult(True, endpoint, "clamd replied PONG", installed)
|
||||
return ProbeResult(False, endpoint,
|
||||
f"unexpected clamd response {response[:80]!r}")
|
||||
f"unexpected clamd response {response[:80]!r}", installed)
|
||||
|
||||
|
||||
def probe_spamd(host: str = "127.0.0.1", port: int = 783,
|
||||
timeout: float = 1.5) -> ProbeResult:
|
||||
endpoint = f"{host}:{port}"
|
||||
installed = shutil.which("spamd") is not None
|
||||
try:
|
||||
with socket.create_connection((host, port), timeout=timeout) as connection:
|
||||
connection.settimeout(timeout)
|
||||
connection.sendall(b"PING SPAMC/1.5\r\n\r\n")
|
||||
response = _receive(connection)
|
||||
except OSError as error:
|
||||
return ProbeResult(False, endpoint, str(error))
|
||||
return ProbeResult(False, endpoint, str(error), installed)
|
||||
first_line = response.splitlines()[0] if response else b""
|
||||
if first_line.startswith(b"SPAMD/") and b" 0 PONG" in first_line:
|
||||
return ProbeResult(True, endpoint, "spamd replied PONG")
|
||||
return ProbeResult(True, endpoint, "spamd replied PONG", installed)
|
||||
return ProbeResult(False, endpoint,
|
||||
f"unexpected spamd response {first_line[:80]!r}")
|
||||
f"unexpected spamd response {first_line[:80]!r}", installed)
|
||||
|
||||
@@ -37,7 +37,7 @@ from .model import (
|
||||
apply_scenario,
|
||||
errors,
|
||||
)
|
||||
from .probes import probe_clamav, probe_spamd
|
||||
from .probes import ProbeResult, probe_clamav, probe_spamd
|
||||
from .storage import ConfigurationError, ConfigurationStore, ValidationError
|
||||
|
||||
|
||||
@@ -50,6 +50,43 @@ def _short(value: Any, maximum: int = 58) -> str:
|
||||
return rendered if len(rendered) <= maximum else rendered[:maximum - 1] + "…"
|
||||
|
||||
|
||||
def _choice_layout(height: int, prompt: str, choice_count: int,
|
||||
selected: int) -> tuple[int, int, int]:
|
||||
"""Return the first choice row, scroll offset and visible row count."""
|
||||
prompt_lines = str(prompt).splitlines() or [""]
|
||||
choice_row = min(4 + len(prompt_lines), height - 3)
|
||||
visible = max(1, height - choice_row - 2)
|
||||
first = max(0, selected - visible + 1)
|
||||
first = min(first, max(0, choice_count - visible))
|
||||
return choice_row, first, visible
|
||||
|
||||
|
||||
def _scanner_choice(dialog: "Dialog", title: str, service: str,
|
||||
result: ProbeResult, example: str) -> bool:
|
||||
if result.available:
|
||||
return dialog.confirm(
|
||||
title, f"{result.detail}. Enable Bongo {service} scanning?", True)
|
||||
if result.installed:
|
||||
return dialog.confirm(
|
||||
title,
|
||||
f"The scanner is installed, but {result.endpoint} is not reachable: "
|
||||
f"{result.detail}.\n\nConfigure Bongo for this endpoint anyway? "
|
||||
f"Start the service and keep its TCP listener on loopback only. "
|
||||
f"See {example}.",
|
||||
False)
|
||||
return False
|
||||
|
||||
|
||||
def _scanner_status(enabled: bool, result: ProbeResult) -> str:
|
||||
if enabled and result.available:
|
||||
return "enabled"
|
||||
if enabled:
|
||||
return "configured (currently unreachable)"
|
||||
if result.installed:
|
||||
return "disabled (service unavailable)"
|
||||
return "disabled (not installed)"
|
||||
|
||||
|
||||
class Dialog:
|
||||
def __init__(self, screen):
|
||||
self.screen = screen
|
||||
@@ -144,16 +181,19 @@ class Dialog:
|
||||
|
||||
def choose(self, title: str, prompt: str,
|
||||
choices: list[tuple[str, str]], selected: int = 0) -> int:
|
||||
if not choices:
|
||||
raise ValueError("at least one choice is required")
|
||||
while True:
|
||||
self._frame(title, (prompt,), "↑/↓ select • Enter accepts • Esc cancels")
|
||||
height, width = self.screen.getmaxyx()
|
||||
first = max(0, min(selected - (height - 9),
|
||||
len(choices) - (height - 7)))
|
||||
choice_row, first, visible = _choice_layout(
|
||||
height, prompt, len(choices), selected)
|
||||
for offset, (_value, label) in enumerate(
|
||||
choices[first:first + height - 7]):
|
||||
choices[first:first + visible]):
|
||||
index = first + offset
|
||||
attribute = curses.A_REVERSE if index == selected else curses.A_NORMAL
|
||||
self.screen.addnstr(5 + offset, 4, label, width - 8, attribute)
|
||||
self.screen.addnstr(choice_row + offset, 4, label,
|
||||
width - 8, attribute)
|
||||
self.screen.refresh()
|
||||
key = self.screen.getch()
|
||||
if key in (10, 13):
|
||||
@@ -307,17 +347,25 @@ def setup_wizard(screen, store: ConfigurationStore):
|
||||
("Checking local clamd and spamd without modifying them…",))
|
||||
clam = probe_clamav()
|
||||
spam = probe_spamd()
|
||||
antivirus = clam.available and dialog.confirm(
|
||||
"ClamAV", f"{clam.detail}. Enable Bongo antivirus scanning?", True)
|
||||
antispam = spam.available and dialog.confirm(
|
||||
"SpamAssassin", f"{spam.detail}. Enable Bongo spam scanning?", True)
|
||||
antivirus = _scanner_choice(
|
||||
dialog, "ClamAV", "antivirus", clam,
|
||||
"/usr/share/bongo/examples/antispam/clamd.conf")
|
||||
antispam = _scanner_choice(
|
||||
dialog, "SpamAssassin", "spam", spam,
|
||||
"/usr/share/bongo/examples/antispam/spamd.options")
|
||||
if not clam.available or not spam.available:
|
||||
missing = []
|
||||
if not clam.available:
|
||||
missing.append(f"ClamAV disabled: {clam.detail}")
|
||||
state = "configured" if antivirus else "disabled"
|
||||
missing.append(
|
||||
f"ClamAV {state}: {clam.endpoint} is unavailable: {clam.detail}")
|
||||
if not spam.available:
|
||||
missing.append(f"SpamAssassin disabled: {spam.detail}")
|
||||
missing.append("Installed examples show safe loopback-only scanner settings.")
|
||||
state = "configured" if antispam else "disabled"
|
||||
missing.append(
|
||||
f"SpamAssassin {state}: {spam.endpoint} is unavailable: {spam.detail}")
|
||||
missing.append(
|
||||
"Examples in /usr/share/bongo/examples/antispam show safe "
|
||||
"loopback-only TCP settings.")
|
||||
dialog.message("Optional scanners", missing)
|
||||
|
||||
password = dialog.input(
|
||||
@@ -372,8 +420,8 @@ def setup_wizard(screen, store: ConfigurationStore):
|
||||
f"Mail proxy: {'enabled' if mail_proxy else 'disabled'}",
|
||||
f"Internal relay: {'enabled' if internal else 'disabled'}",
|
||||
f"LDAP: {'enabled' if ldap else 'disabled'}",
|
||||
f"SpamAssassin: {'enabled' if antispam else 'disabled'}",
|
||||
f"ClamAV: {'enabled' if antivirus else 'disabled'}",
|
||||
f"SpamAssassin: {_scanner_status(antispam, spam)}",
|
||||
f"ClamAV: {_scanner_status(antivirus, clam)}",
|
||||
]
|
||||
if not dialog.confirm("Apply setup", "\n".join(summary) + "\n\nInstall now?", True):
|
||||
raise Cancelled()
|
||||
|
||||
Reference in New Issue
Block a user