Skip to content

Commit fca5548

Browse files
committed
test(auth): remove ui test skips and ensure proper password auth testing
- Fixed PHP config parsing regex to prevent __metadata fields from interfering with API_TOKEN and SETPWD booleans. - Re-enabled hard assertions instead of skips for login tests. - Made test_ui_login.py securely and atomically toggle SETPWD_enable_password via python during setup to force UI login triggers. - Updated maintenance.php UI test to initialize appConfig correctly. - Added missing host args to fritzbox tests. - Prevented test_ui_login fixture from accidentally stripping API_TOKEN from app.conf. - Added explicit invalidation of SETTINGS_SECONDARYCACHE in server/initialise.py. - Fixed getSettingValue fallback logic in PHP auth layers to reliably use the token generated by the backend.
1 parent 92112a2 commit fca5548

18 files changed

Lines changed: 257 additions & 126 deletions

.env

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ PORT=20211
1313
# LDAP_SERVER=ldap.example.com
1414
# LDAP_PORT=389
1515
# LDAP_USE_SSL=false
16-
# LDAP_USE_START_TLS=false
16+
# LDAP_USE_START_TLS=true
1717
# LDAP_TLS_VERIFY_CERT=true
1818
# LDAP_CA_CERT_PATH=/etc/ssl/certs/ca-certificates.crt
1919
# LDAP_DISABLE_LOCAL_ADMIN=false

front/index.php

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<?php
55

66
require_once $_SERVER['DOCUMENT_ROOT'].'/php/server/db.php';
7+
require_once $_SERVER['DOCUMENT_ROOT'].'/php/server/util.php';
78
require_once $_SERVER['DOCUMENT_ROOT'].'/php/templates/language/lang.php';
89
require_once $_SERVER['DOCUMENT_ROOT'].'/php/templates/security.php';
910

@@ -37,16 +38,17 @@
3738
} else {
3839
$ldap_enabled_line = getConfigLine('/^LDAP_enabled.*=/', $configLines);
3940
if ($ldap_enabled_line !== null && isset($ldap_enabled_line[1])) {
40-
$ldap_enabled = strtolower(trim($ldap_enabled_line[1])) === 'true';
41+
$ldap_enabled_value = strtolower(trim($ldap_enabled_line[1]));
42+
$ldap_enabled = $ldap_enabled_value === 'true' || $ldap_enabled_value === '1';
4143
}
4244
}
4345

4446
/**
4547
* Derive the Python API port from the GRAPHQL_PORT setting in app.conf.
46-
* Falls back to 20211 (the default) when not set.
48+
* Falls back to 20212 (the default) when not set.
4749
*/
4850
$gql_line = getConfigLine('/^GRAPHQL_PORT.*=/', $configLines);
49-
$graphql_port = 20211;
51+
$graphql_port = 20212;
5052
if ($gql_line !== null && isset($gql_line[1])) {
5153
$parsed_port = (int) preg_replace('/[^0-9]/', '', $gql_line[1]);
5254
if ($parsed_port >= 1 && $parsed_port <= 65535) {
@@ -129,8 +131,16 @@ function login_user(): void {
129131
session_regenerate_id(true);
130132
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
131133

134+
$resolved_api_token = !empty($api_token)
135+
? $api_token
136+
: (function_exists('getSettingValue') ? getSettingValue('API_TOKEN') : '');
137+
138+
if (empty($resolved_api_token)) {
139+
throw new RuntimeException('API_TOKEN is not configured');
140+
}
141+
132142
// Set remember-me cookie with HMAC (not raw password hash)
133-
$cookie_value = hash_hmac('sha256', $nax_Password, $api_token);
143+
$cookie_value = hash_hmac('sha256', $nax_Password, $resolved_api_token);
134144
setcookie(COOKIE_SAVE_LOGIN_NAME, $cookie_value, [
135145
'expires' => time() + 3600 * 24 * 7,
136146
'path' => '/',
@@ -174,6 +184,14 @@ function logout_user(): void {
174184
if ($ldap_enabled) {
175185
// LDAP path: delegate credential validation to the Python API.
176186
// The API token is required so only server-side callers can reach the endpoint.
187+
$resolved_api_token = !empty($api_token)
188+
? $api_token
189+
: (function_exists('getSettingValue') ? getSettingValue('API_TOKEN') : '');
190+
191+
if (empty($resolved_api_token)) {
192+
throw new RuntimeException('API_TOKEN is not configured');
193+
}
194+
177195
$ldap_payload = json_encode([
178196
'username' => isset($_POST['loginusername']) ? trim($_POST['loginusername']) : '',
179197
'password' => $_POST['loginpassword'],
@@ -182,7 +200,7 @@ function logout_user(): void {
182200
'http' => [
183201
'method' => 'POST',
184202
'header' => "Content-Type: application/json\r\n"
185-
. "Authorization: Bearer " . $api_token . "\r\n"
203+
. "Authorization: Bearer " . $resolved_api_token . "\r\n"
186204
. "X-Forwarded-For: " . ($_SERVER['REMOTE_ADDR'] ?? '127.0.0.1') . "\r\n",
187205
'content' => $ldap_payload,
188206
'timeout' => 5,

front/php/server/query_graphql.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
// Helper function to get GraphQL URL (you can replace this with environment variables)
1313
function getGraphQLUrl() {
1414
$port = getSettingValue("GRAPHQL_PORT"); // Port for the GraphQL server
15+
if (empty($port) || !is_numeric($port)) {
16+
$port = 20211;
17+
}
1518
return "0.0.0.0:$port/graphql"; // Full URL to the GraphQL endpoint
1619
}
1720

front/php/templates/auth.php

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,26 @@
2929
}
3030

3131
$config_file_lines = file($config_file);
32-
$config_file_lines_pw = array_values(preg_grep('/^SETPWD_password.*=/', $config_file_lines));
33-
$password_line = explode("'", $config_file_lines_pw[0]);
32+
$config_file_lines_pw = array_values(preg_grep('/^SETPWD_password\s*=/', $config_file_lines ?: []));
33+
if (empty($config_file_lines_pw) || substr_count($config_file_lines_pw[0], "'") < 2) {
34+
http_response_code(401);
35+
echo 'Unauthorized 401';
36+
exit;
37+
}
38+
$password_line = explode("'", $config_file_lines_pw[0], 3);
3439
$nax_Password = $password_line[1];
3540

36-
$config_file_lines_token = array_values(preg_grep('/^API_TOKEN.*=/', $config_file_lines));
37-
$api_token = !empty($config_file_lines_token) ? explode("'", $config_file_lines_token[0])[1] : '';
41+
$config_file_lines_token = array_values(preg_grep('/^API_TOKEN\s*=/', $config_file_lines ?: []));
42+
$token_line = !empty($config_file_lines_token) ? explode("'", $config_file_lines_token[0], 3) : [];
43+
$api_token = $token_line[1] ?? '';
44+
if (empty($api_token)) {
45+
$api_token = getenv('API_TOKEN') ?: '';
46+
}
47+
if ($api_token === '') {
48+
http_response_code(401);
49+
echo 'Unauthorized 401';
50+
exit;
51+
}
3852

3953
$expected_cookie = hash_hmac('sha256', $nax_Password, $api_token);
4054
if (isset($_COOKIE[$CookieSaveLoginName]) && hash_equals($expected_cookie, $_COOKIE[$CookieSaveLoginName])) {

front/php/templates/security.php

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ function redirect($url) {
7575
$configLines = file(CONFIG_PATH);
7676

7777
// Handle web protection and password
78-
$nax_WebProtection = strtolower(trim(getConfigLine('/^SETPWD_enable_password.*=/', $configLines)[1] ?? 'false'));
78+
$nax_WebProtection = strtolower(trim(getConfigValue('/^SETPWD_enable_password\s*=/', $configLines)));
7979

8080
$ldap_enabled = false;
8181
$env_ldap = getenv('LDAP_ENABLED');
@@ -84,14 +84,18 @@ function redirect($url) {
8484
if ($env_ldap !== false && $env_ldap !== '') {
8585
$ldap_enabled = strtolower(trim($env_ldap)) === 'true' || trim($env_ldap) === '1';
8686
} else {
87-
$ldap_enabled = strtolower(trim(getConfigLine('/^LDAP_enabled.*=/', $configLines)[1] ?? 'false')) === 'true';
87+
$val = strtolower(trim(getConfigValue('/^LDAP_enabled\s*=/', $configLines)));
88+
$ldap_enabled = $val === 'true' || $val === '1';
8889
}
8990

9091
if ($ldap_enabled) {
9192
$nax_WebProtection = 'true';
9293
}
93-
$nax_Password = getConfigValue('/^SETPWD_password.*=/', $configLines);
94-
$api_token = getConfigValue('/^API_TOKEN.*=/', $configLines, "'");
94+
$nax_Password = getConfigValue('/^SETPWD_password\s*=/', $configLines);
95+
$api_token = getConfigValue('/^API_TOKEN\s*=/', $configLines, "'");
96+
if (empty($api_token)) {
97+
$api_token = getenv('API_TOKEN') ?: '';
98+
}
9599

96100
$expectedToken = 'Bearer ' . $api_token;
97101

scripts/run_tests_in_docker_environment.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ docker exec netalertx-test-container /bin/bash -c " \
8080
# --- 9. Execute Tests ---
8181
echo "--- Executing tests inside the container ---"
8282
docker exec netalertx-test-container /bin/bash -c " \
83-
cd /workspaces/NetAlertX && pytest -m 'not (docker or compose or feature_complete)' --cache-clear -o cache_dir=/tmp/.pytest_cache; \
83+
cd /workspaces/NetAlertX && pytest test/ -m 'not (docker or compose or feature_complete)' --cache-clear -o cache_dir=/tmp/.pytest_cache; \
8484
"
8585

8686
# --- 10. Final Teardown ---

server/__main__.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,6 @@ def main():
128128

129129
# proceed if 1 minute passed
130130
if conf.last_scan_run + datetime.timedelta(minutes=1) < conf.loop_start_time:
131-
is_idle_this_loop = False
132131
# last time any scan or maintenance/upkeep was run
133132
conf.last_scan_run = loop_start_time
134133

@@ -166,6 +165,7 @@ def main():
166165
mylog("debug", [f"[MAIN] processScan: {processScan}"])
167166

168167
if processScan is True:
168+
is_idle_this_loop = False
169169
mylog("debug", "[MAIN] start processing scan results")
170170
process_scan(db)
171171
updateState("Scan processed", None, None, None, None, False)
@@ -275,8 +275,11 @@ def main():
275275
mylog("verbose", ["[MAIN] System is fully idle. All processes finished."])
276276
now = time.time()
277277
if now - last_idle_notification_ts > (IDLE_NOTIFICATION_COOLDOWN_HOURS * 3600):
278-
write_notification("All processes are idle", "info")
279-
last_idle_notification_ts = now
278+
try:
279+
write_notification("All processes are idle", "info")
280+
last_idle_notification_ts = now
281+
except Exception as e:
282+
mylog("none", [f"[MAIN] Failed to write idle notification: {e}"])
280283
was_idle = True
281284
elif not is_idle_this_loop:
282285
was_idle = False

server/api_server/api_server_start.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1965,7 +1965,7 @@ def _get_client_ip():
19651965
"""Return the client IP, trusting X-Forwarded-For only from local/proxy callers."""
19661966
remote_addr = request.remote_addr or ""
19671967
forwarded = request.headers.get("X-Forwarded-For", "")
1968-
trusted_proxies = {"127.0.0.1", "::1", "localhost"}
1968+
trusted_proxies = {"127.0.0.1", "::1"}
19691969
if forwarded and remote_addr in trusted_proxies:
19701970
return forwarded.split(",")[0].strip()
19711971
return remote_addr
@@ -2030,8 +2030,7 @@ def api_auth_login(payload=None):
20302030
if result.success:
20312031
# Clear failures on success
20322032
with _failed_logins_lock:
2033-
if client_ip in FAILED_LOGINS:
2034-
del FAILED_LOGINS[client_ip]
2033+
FAILED_LOGINS.pop(client_ip, None)
20352034
return jsonify({
20362035
"success": True,
20372036
"message": "Authentication successful",

server/auth/ldap_provider.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ class LdapProvider(AuthProvider):
8585
"""Authenticate against an LDAP / Active Directory server."""
8686

8787
name = "ldap"
88+
USER_NOT_FOUND = "User not found"
8889

8990
# ------------------------------------------------------------------
9091
# Public interface
@@ -131,7 +132,7 @@ def authenticate(self, username: str, password: str) -> AuthResult:
131132
user_dn = self._resolve_user_dn(ldap3, server_obj, cfg, username)
132133

133134
if user_dn is None:
134-
return AuthResult.fail(self.name, "User not found")
135+
return AuthResult.fail(self.name, self.USER_NOT_FOUND)
135136

136137
return self._bind_as_user(ldap3, server_obj, cfg, user_dn, username, password)
137138

@@ -287,7 +288,7 @@ def _resolve_user_dn(self, ldap3, server_obj, cfg: dict, username: str) -> Optio
287288
entries = conn.entries
288289
if len(entries) != 1:
289290
mylog("verbose", [
290-
f"[auth.ldap] User '{username}' not found "
291+
f"[auth.ldap] User '{_sanitize_for_log(username)}' not found "
291292
f"(got {len(entries)} entries for filter {search_filter})"
292293
])
293294
return None
@@ -316,7 +317,7 @@ def _bind_as_user(
316317
if bind_success:
317318
return AuthResult.ok(username, self.name)
318319

319-
mylog("verbose", [f"[auth.ldap] User bind failed for DN '{user_dn}': {conn.result}"])
320+
mylog("verbose", [f"[auth.ldap] User bind failed for DN '{_sanitize_for_log(user_dn)}': {conn.result}"])
320321
return AuthResult.fail(self.name)
321322

322323
finally:

server/auth/manager.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,10 @@ def authenticate(self, username: str, password: str) -> AuthResult:
5050
return ldap_result
5151

5252
# If the user doesn't exist in LDAP, we can fallback to local auth
53-
if ldap_result.error == "User not found":
53+
if ldap_result.error == LdapProvider.USER_NOT_FOUND:
5454
if not disable_local:
55+
# Only the built-in local admin is a recovery account;
56+
# all other identities must exist in LDAP.
5557
if username != "admin":
5658
mylog("verbose", [f"[auth.manager] Local fallback denied for non-admin user '{_sanitize_for_log(username)}'"])
5759
return ldap_result

0 commit comments

Comments
 (0)