Add better jail configuration: file CRUD, enable/disable, log paths

Task 4 (Better Jail Configuration) implementation:
- Add fail2ban_config_dir setting to app/config.py
- New file_config_service: list/view/edit/create jail.d, filter.d, action.d files
  with path-traversal prevention and 512 KB content size limit
- New file_config router: GET/PUT/POST endpoints for jail files, filter files,
  and action files; PUT .../enabled for toggle on/off
- Extend config_service with delete_log_path() and add_log_path()
- Add DELETE /api/config/jails/{name}/logpath and POST /api/config/jails/{name}/logpath
- Extend geo router with re-resolve endpoint; add geo_re_resolve background task
- Update blocklist_service with revised scheduling helpers
- Update Docker compose files with BANGUI_FAIL2BAN_CONFIG_DIR env var and
  rw volume mount for the fail2ban config directory
- Frontend: new Jail Files, Filters, Actions tabs in ConfigPage; file editor
  with accordion-per-file, editable textarea, save/create; add/delete log paths
- Frontend: types in types/config.ts; API calls in api/config.ts and api/endpoints.ts
- 63 new backend tests (test_file_config_service, test_file_config, test_geo_re_resolve)
- 6 new frontend tests in ConfigPageLogPath.test.tsx
- ruff, mypy --strict, tsc --noEmit, eslint: all clean; 617 backend tests pass
This commit is contained in:
2026-03-12 20:08:33 +01:00
parent 59464a1592
commit ea35695221
23 changed files with 2911 additions and 91 deletions

View File

@@ -16,9 +16,94 @@ import { ConfigPage } from "../../pages/ConfigPage";
import type { JailConfig } from "../../types/config";
// ---------------------------------------------------------------------------
// Module mocks
// Module mocks — use vi.hoisted so refs are available when vi.mock runs
// ---------------------------------------------------------------------------
const {
mockAddLogPath,
mockDeleteLogPath,
mockUpdateJailConfig,
mockReloadConfig,
mockFetchGlobalConfig,
mockFetchServerSettings,
mockFetchJailConfigs,
mockFetchMapColorThresholds,
mockFetchJailConfigFiles,
mockFetchFilterFiles,
mockFetchActionFiles,
mockUpdateMapColorThresholds,
mockUpdateGlobalConfig,
mockUpdateServerSettings,
mockFlushLogs,
mockSetJailConfigFileEnabled,
} = vi.hoisted(() => ({
mockAddLogPath: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
mockDeleteLogPath: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
mockUpdateJailConfig: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
mockReloadConfig: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
mockFetchGlobalConfig: vi.fn().mockResolvedValue({
config: {
ban_time: 600,
max_retry: 5,
find_time: 300,
backend: "auto",
},
}),
mockFetchServerSettings: vi.fn().mockResolvedValue({
settings: {
log_level: "INFO",
log_target: "STDOUT",
syslog_socket: null,
db_path: "/var/lib/fail2ban/fail2ban.sqlite3",
db_purge_age: 86400,
db_max_matches: 10,
},
}),
mockFetchJailConfigs: vi.fn(),
mockFetchMapColorThresholds: vi.fn().mockResolvedValue({
threshold_high: 100,
threshold_medium: 50,
threshold_low: 20,
}),
mockFetchJailConfigFiles: vi.fn().mockResolvedValue({ files: [] }),
mockFetchFilterFiles: vi.fn().mockResolvedValue({ files: [] }),
mockFetchActionFiles: vi.fn().mockResolvedValue({ files: [] }),
mockUpdateMapColorThresholds: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
mockUpdateGlobalConfig: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
mockUpdateServerSettings: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
mockFlushLogs: vi.fn().mockResolvedValue({ message: "ok" }),
mockSetJailConfigFileEnabled: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
}));
vi.mock("../../api/config", () => ({
addLogPath: mockAddLogPath,
deleteLogPath: mockDeleteLogPath,
fetchJailConfigs: mockFetchJailConfigs,
fetchJailConfig: vi.fn(),
updateJailConfig: mockUpdateJailConfig,
reloadConfig: mockReloadConfig,
fetchGlobalConfig: mockFetchGlobalConfig,
updateGlobalConfig: mockUpdateGlobalConfig,
fetchServerSettings: mockFetchServerSettings,
updateServerSettings: mockUpdateServerSettings,
flushLogs: mockFlushLogs,
fetchMapColorThresholds: mockFetchMapColorThresholds,
updateMapColorThresholds: mockUpdateMapColorThresholds,
fetchJailConfigFiles: mockFetchJailConfigFiles,
fetchJailConfigFileContent: vi.fn(),
setJailConfigFileEnabled: mockSetJailConfigFileEnabled,
fetchFilterFiles: mockFetchFilterFiles,
fetchFilterFile: vi.fn(),
updateFilterFile: vi.fn(),
createFilterFile: vi.fn(),
fetchActionFiles: mockFetchActionFiles,
fetchActionFile: vi.fn(),
updateActionFile: vi.fn(),
createActionFile: vi.fn(),
previewLog: vi.fn(),
testRegex: vi.fn(),
}));
/** Minimal jail fixture used across tests. */
const MOCK_JAIL: JailConfig = {
name: "sshd",
@@ -34,77 +119,6 @@ const MOCK_JAIL: JailConfig = {
actions: [],
};
const mockAddLogPath = vi.fn().mockResolvedValue(undefined);
const mockDeleteLogPath = vi.fn().mockResolvedValue(undefined);
const mockUpdateJailConfig = vi.fn().mockResolvedValue(undefined);
const mockReloadConfig = vi.fn().mockResolvedValue(undefined);
const mockFetchGlobalConfig = vi.fn().mockResolvedValue({
config: {
ban_time: 600,
max_retry: 5,
find_time: 300,
backend: "auto",
},
});
const mockFetchServerSettings = vi.fn().mockResolvedValue({
settings: {
log_level: "INFO",
log_target: "STDOUT",
syslog_socket: null,
db_path: "/var/lib/fail2ban/fail2ban.sqlite3",
db_purge_age: 86400,
db_max_matches: 10,
},
});
const mockFetchJailConfigs = vi.fn().mockResolvedValue({
jails: [MOCK_JAIL],
total: 1,
});
const mockFetchMapColorThresholds = vi.fn().mockResolvedValue({
threshold_high: 100,
threshold_medium: 50,
threshold_low: 20,
});
const mockFetchJailConfigFiles = vi.fn().mockResolvedValue({ files: [] });
const mockFetchFilterFiles = vi.fn().mockResolvedValue({ files: [] });
const mockFetchActionFiles = vi.fn().mockResolvedValue({ files: [] });
const mockUpdateMapColorThresholds = vi.fn().mockResolvedValue({});
const mockUpdateGlobalConfig = vi.fn().mockResolvedValue(undefined);
const mockUpdateServerSettings = vi.fn().mockResolvedValue(undefined);
const mockFlushLogs = vi.fn().mockResolvedValue({ message: "ok" });
const mockSetJailConfigFileEnabled = vi.fn().mockResolvedValue(undefined);
vi.mock("../../api/config", () => ({
addLogPath: (...args: unknown[]) => mockAddLogPath(...args),
deleteLogPath: (...args: unknown[]) => mockDeleteLogPath(...args),
fetchJailConfigs: () => mockFetchJailConfigs(),
fetchJailConfig: vi.fn(),
updateJailConfig: (...args: unknown[]) => mockUpdateJailConfig(...args),
reloadConfig: () => mockReloadConfig(),
fetchGlobalConfig: () => mockFetchGlobalConfig(),
updateGlobalConfig: (...args: unknown[]) => mockUpdateGlobalConfig(...args),
fetchServerSettings: () => mockFetchServerSettings(),
updateServerSettings: (...args: unknown[]) => mockUpdateServerSettings(...args),
flushLogs: () => mockFlushLogs(),
fetchMapColorThresholds: () => mockFetchMapColorThresholds(),
updateMapColorThresholds: (...args: unknown[]) =>
mockUpdateMapColorThresholds(...args),
fetchJailConfigFiles: () => mockFetchJailConfigFiles(),
fetchJailConfigFileContent: vi.fn(),
setJailConfigFileEnabled: (...args: unknown[]) =>
mockSetJailConfigFileEnabled(...args),
fetchFilterFiles: () => mockFetchFilterFiles(),
fetchFilterFile: vi.fn(),
updateFilterFile: vi.fn(),
createFilterFile: vi.fn(),
fetchActionFiles: () => mockFetchActionFiles(),
fetchActionFile: vi.fn(),
updateActionFile: vi.fn(),
createActionFile: vi.fn(),
previewLog: vi.fn(),
testRegex: vi.fn(),
}));
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------