Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/run-all-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ on:
description: '📂 docker_tests/ (Environment & PUID/PGID)'
type: boolean
default: false
run_ldap:
description: '🔑 LDAP (Auth plugins, unit and UI tests)'
type: boolean
default: true
Comment thread
coderabbitai[bot] marked this conversation as resolved.
run_ui:
description: '📂 ui/ (Selenium & Dashboard)'
type: boolean
Expand Down Expand Up @@ -62,6 +66,7 @@ jobs:
if [ "${{ github.event.inputs.run_api }}" == "true" ]; then PATHS="$PATHS test/api_endpoints/ test/server/"; fi
if [ "${{ github.event.inputs.run_backend }}" == "true" ]; then PATHS="$PATHS test/backend/ test/db/"; fi
if [ "${{ github.event.inputs.run_docker_env }}" == "true" ]; then PATHS="$PATHS test/docker_tests/"; fi
if [ "${{ github.event.inputs.run_ldap }}" == "true" ]; then PATHS="$PATHS test/server/test_ldap_provider.py test/server/test_ldap_provider_env.py test/ui/test_ui_ldap_login.py test/docker_tests/test_ldap_ui.py"; fi
if [ "${{ github.event.inputs.run_ui }}" == "true" ]; then PATHS="$PATHS test/ui/"; fi
if [ "${{ github.event.inputs.run_plugins }}" == "true" ]; then PATHS="$PATHS test/plugins/"; fi

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,4 @@ docker-compose.yml.ffsb42
test_mounts/
.gemini/settings.json
.vscode/mcp.json
pr_1621_open_review_comments.md
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove before merge

107 changes: 98 additions & 9 deletions front/index.php
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
<!-- NetAlertX CSS -->
<link rel="stylesheet" href="css/app.css">
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why was this removed?


<?php

require_once $_SERVER['DOCUMENT_ROOT'].'/php/server/db.php';
require_once $_SERVER['DOCUMENT_ROOT'].'/php/templates/language/lang.php';
require_once $_SERVER['DOCUMENT_ROOT'].'/php/templates/security.php';

if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

// if (session_status() === PHP_SESSION_NONE) {
// session_start();
// }
Expand All @@ -15,6 +16,29 @@

const DEFAULT_REDIRECT = '/devices.php';

/* =====================================================
LDAP Configuration
$configLines and $api_token are already loaded by security.php
===================================================== */

// Config file is the single source of truth (Python backend resolves env vars at startup)
$ldap_enabled = strtolower(trim(getConfigLine('/^LDAP_enabled\s*=/', $configLines)[1] ?? 'false')) === 'true';

/**
* Derive the Python API port from the GRAPHQL_PORT setting in app.conf.
* Falls back to 20212 (the default) when not set.
*/
$gql_line = getConfigLine('/^GRAPHQL_PORT.*=/', $configLines);
$graphql_port = 20212;
if ($gql_line !== null && isset($gql_line[1])) {
$parsed_port = (int) preg_replace('/[^0-9]/', '', $gql_line[1]);
if ($parsed_port >= 1 && $parsed_port <= 65535) {
$graphql_port = $parsed_port;
}
}

$ldap_login_url = "http://127.0.0.1:{$graphql_port}/api/auth/login";
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/* =====================================================
Helper Functions
===================================================== */
Expand Down Expand Up @@ -83,8 +107,24 @@ function is_authenticated(): bool {
}

function login_user(): void {
global $nax_Password, $api_token, $configLines;

$_SESSION['login'] = 1;
session_regenerate_id(true);
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));

// Set remember-me cookie with HMAC when API_TOKEN is available.
// On first boot the token may not exist yet — skip the cookie gracefully.
if (!empty($api_token)) {
$cookie_value = hash_hmac('sha256', $nax_Password, $api_token);
setcookie(COOKIE_SAVE_LOGIN_NAME, $cookie_value, [
'expires' => time() + 3600 * 24 * 7,
'path' => '/',
'httponly' => true,
'secure' => !empty($_SERVER['HTTPS']),
'samesite' => 'Strict',
]);
}
}


Expand Down Expand Up @@ -114,16 +154,51 @@ function logout_user(): void {
Login Attempt
===================================================== */

if (!empty($_POST['loginpassword'])) {
if (!empty($_POST['loginpassword']) &&
isset($_POST['csrf_token']) &&
hash_equals($_SESSION['csrf_token'] ?? '', $_POST['csrf_token'])) {

$incomingHash = hash('sha256', $_POST['loginpassword']);
if ($ldap_enabled) {
// LDAP path: delegate credential validation to the Python API.
// The API token is required so only server-side callers can reach the endpoint.
if (empty($api_token)) {
throw new RuntimeException('API_TOKEN is not configured');
}

if (hash_equals($nax_Password, $incomingHash)) {
$ldap_payload = json_encode([
'username' => isset($_POST['loginusername']) ? trim($_POST['loginusername']) : '',
'password' => $_POST['loginpassword'],
]);
$stream_opts = [
'http' => [
'method' => 'POST',
'header' => "Content-Type: application/json\r\n"
. "Authorization: Bearer " . $api_token . "\r\n"
. "X-Forwarded-For: " . ($_SERVER['REMOTE_ADDR'] ?? '127.0.0.1') . "\r\n",
'content' => $ldap_payload,
'timeout' => 5,
'ignore_errors' => true,
]
];
$ctx = stream_context_create($stream_opts);
$raw = @file_get_contents($ldap_login_url, false, $ctx);
$api_resp = ($raw !== false) ? @json_decode($raw, true) : null;

if (is_array($api_resp) && $api_resp['success'] === true) {
login_user();
safe_redirect(append_hash($redirectTo));
}
// Fall through to show the login form with an error state.
} else {
// Local path: compare SHA-256 digest against the stored hash (same as before).
$incomingHash = hash('sha256', $_POST['loginpassword']);

login_user();
if (hash_equals($nax_Password, $incomingHash)) {
login_user();

// Redirect to target page, preserving deep link hash if present
safe_redirect(append_hash($redirectTo));
// Redirect to target page, preserving deep link hash if present
safe_redirect(append_hash($redirectTo));
}
}
}

Expand Down Expand Up @@ -179,6 +254,9 @@ function logout_user(): void {
<!-- Favicon -->
<link id="favicon" rel="icon" type="image/x-icon" href="img/NetAlertX_logo.png">
<link rel="stylesheet" href="/css/offline-font.css">

<!-- NetAlertX CSS -->
<link rel="stylesheet" href="css/app.css">
</head>
<body class="hold-transition login-page col-sm-12 col-sx-12">
<div class="login-box login-custom">
Expand All @@ -193,8 +271,19 @@ function logout_user(): void {
? '?next=' . htmlspecialchars($_GET['next'], ENT_QUOTES, 'UTF-8')
: '';
?>" method="post">
<?php if ($ldap_enabled): ?>
<div class="form-group has-feedback">
<input type="text" class="form-control"
placeholder="<?= lang('Login_Username');?>"
name="loginusername"
autocomplete="username"
required>
<span class="glyphicon glyphicon-user form-control-feedback"></span>
</div>
<?php endif; ?>
<div class="form-group has-feedback">
<input type="hidden" name="url_hash" id="url_hash">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token'], ENT_QUOTES, 'UTF-8') ?>">
<input type="password" class="form-control" placeholder="<?= lang('Login_Psw-box');?>" name="loginpassword">
<span class="glyphicon glyphicon-lock form-control-feedback"></span>
</div>
Expand Down
2 changes: 1 addition & 1 deletion front/js/app-init.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ function isAppInitialized() {
}

// check if all required languages chached
if(parseInt(getCache(CACHE_KEYS.STRINGS_COUNT)) != lang_shouldBeCompletedCalls)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why was this changed?

if(parseInt(getCache(CACHE_KEYS.STRINGS_COUNT)) < lang_shouldBeCompletedCalls)
{
_isAppInitLog(`[isAppInitialized] waiting on cacheStrings: ${getCache(CACHE_KEYS.STRINGS_COUNT)} of ${lang_shouldBeCompletedCalls}`);
return false;
Expand Down
3 changes: 3 additions & 0 deletions front/php/server/query_graphql.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
// Helper function to get GraphQL URL (you can replace this with environment variables)
function getGraphQLUrl() {
$port = getSettingValue("GRAPHQL_PORT"); // Port for the GraphQL server
if (empty($port) || !is_numeric($port)) {
$port = 20211;
}
return "0.0.0.0:$port/graphql"; // Full URL to the GraphQL endpoint
}

Expand Down
24 changes: 21 additions & 3 deletions front/php/templates/auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,29 @@
}

$config_file_lines = file($config_file);
$config_file_lines = array_values(preg_grep('/^SETPWD_password.*=/', $config_file_lines));
$password_line = explode("'", $config_file_lines[0]);
$config_file_lines_pw = array_values(preg_grep('/^SETPWD_password\s*=/', $config_file_lines ?: []));
if (empty($config_file_lines_pw) || substr_count($config_file_lines_pw[0], "'") < 2) {
http_response_code(401);
echo 'Unauthorized 401';
exit;
}
$password_line = explode("'", $config_file_lines_pw[0], 3);
$nax_Password = $password_line[1];

if (isset($_COOKIE[$CookieSaveLoginName]) && $nax_Password === $_COOKIE[$CookieSaveLoginName]) {
$config_file_lines_token = array_values(preg_grep('/^API_TOKEN\s*=/', $config_file_lines ?: []));
$token_line = !empty($config_file_lines_token) ? explode("'", $config_file_lines_token[0], 3) : [];
$api_token = $token_line[1] ?? '';
if (empty($api_token)) {
$api_token = getenv('API_TOKEN') ?: '';
Comment thread
adamoutler marked this conversation as resolved.
}
if ($api_token === '') {
http_response_code(401);
echo 'Unauthorized 401';
exit;
}

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

Expand Down
6 changes: 6 additions & 0 deletions front/php/templates/language/en_us.json
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,11 @@
"Device_NoData_Scanning": "Waiting for the first scan - this may take several minutes after the initial setup.",
"Device_NoData_Title": "No devices found yet",
"Device_NoMatch_Title": "No devices match the current filter",
"LDAP library not available": "LDAP library not available",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lang keys should have no spaces - not tested - replace spaces with underscores _ - @coderabbitai - add learning

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jokob-sk, noted! All language keys should use underscores instead of spaces. There are 5 affected keys in this PR that need renaming:

Current key (with spaces) Suggested key (with underscores)
"LDAP library not available" "LDAP_library_not_available"
"LDAP server not configured" "LDAP_server_not_configured"
"User not found" "User_not_found"
"User not found or search failed" "User_not_found_or_search_failed"
"Username and password are required" "Username_and_password_are_required"

Any PHP, Python, or JS code referencing these keys by the old names (e.g. in ldap_provider.py, manager.py, or frontend templates) will also need to be updated to use the new underscore-based keys.


✏️ Learnings added
Learnt from: jokob-sk
Repo: netalertx/NetAlertX PR: 1621
File: front/php/templates/language/en_us.json:214-214
Timestamp: 2026-05-07T22:02:36.144Z
Learning: In NetAlertX, language/translation keys in `front/php/templates/language/en_us.json` must not contain spaces. All words in a key should be separated by underscores (`_`). For example, `"LDAP library not available"` is incorrect and should be `"LDAP_library_not_available"`. Any code referencing these keys (PHP, Python, JS) must use the underscore form. This applies to all locale files in the language templates directory.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: jokob-sk
Repo: netalertx/NetAlertX PR: 1435
File: front/php/templates/language/sv_sv.json:300-305
Timestamp: 2026-01-20T23:52:58.899Z
Learning: Ignore non-English translation files in the language templates directory when they contain empty translations; treat en_us.json as the source of truth and rely on Weblate to manage translations. In reviews, do not require edits to sv_sv.json, de_de.json, etc., unless there is an actual missing source key in en_us.json. Validate that non-English files do not contain meaningful translation values and that en_us.json remains the canonical source for strings.

"LDAP server not configured": "LDAP server not configured",
"User not found": "User not found",
"User not found or search failed": "User not found or search failed",
"Username and password are required": "Username and password are required",
"Device_Save_Failed": "Failed to save device",
"Device_Save_Unauthorized": "Unauthorized - invalid API token",
"Device_Saved_Success": "Device saved successfully",
Expand Down Expand Up @@ -398,6 +403,7 @@
"Login_Remember": "Remember",
"Login_Remember_small": "(valid for 7 days)",
"Login_Submit": "Log in",
"Login_Username": "Username",
"Login_Toggle_Alert_headline": "Password Alert!",
"Login_Toggle_Info": "Password Information",
"Login_Toggle_Info_headline": "Password Information",
Expand Down
18 changes: 16 additions & 2 deletions front/php/templates/security.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,17 @@ function redirect($url) {
// Handle logout
if (!empty($_REQUEST['action']) && $_REQUEST['action'] == 'logout') {
session_destroy();
setcookie(COOKIE_SAVE_LOGIN_NAME, "", time() - 3600);

// Determine protocol for secure cookie flag
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https://' : 'http://';

setcookie(COOKIE_SAVE_LOGIN_NAME, "", [
'expires' => time() - 3600,
'path' => '/',
'httponly' => true,
'secure' => $protocol === 'https://',
'samesite' => 'Strict',
]);
redirect('index.php');
}

Expand All @@ -66,14 +76,18 @@ function redirect($url) {

// Handle web protection and password
$nax_WebProtection = strtolower(trim(getConfigLine('/^SETPWD_enable_password.*=/', $configLines)[1] ?? 'false'));

// Auth plugins force web protection when enabled
if (strtolower(trim(getConfigLine('/^LDAP_enabled\s*=/', $configLines)[1] ?? 'false')) === 'true') $nax_WebProtection = 'true';

$nax_Password = getConfigValue('/^SETPWD_password.*=/', $configLines);
$api_token = getConfigValue('/^API_TOKEN.*=/', $configLines, "'");

$expectedToken = 'Bearer ' . $api_token;

// Authentication Handling
if ($nax_WebProtection == 'true') {
if ($authHeader === $expectedToken) {
if (!empty($api_token) && $authHeader === $expectedToken) {
$_SESSION['login'] = 1; // User authenticated with bearer token
} elseif (!empty($authHeader)) {
echo "[Security] Incorrect Bearer Token";
Expand Down
Loading
Loading