The Authelia config default pointed at /mnt/user/appdata/Authelia, which does not exist — the auth stack lives in the Critical-Data share so it gets synced. Because conf_populate only fills empty fields, that wrong non-empty default blocked auto-detection permanently and left the auth page's rules panel dead. Also corrects three schedule comments that no longer matched varaverk.cron and two references to scripts that have since been renamed or split.
419 lines
17 KiB
PHP
419 lines
17 KiB
PHP
<?php
|
|
require_once __DIR__ . '/config.php';
|
|
|
|
// ── Config ────────────────────────────────────────────────────────────────────
|
|
|
|
function vv_auth_conf(): array {
|
|
$v = vv_conf_vars();
|
|
$host = strtoupper(vv_detect_host());
|
|
return [
|
|
'npm_url' => rtrim($v["{$host}_NPM_URL"] ?? 'http://localhost:7818', '/'),
|
|
'npm_user' => $v["{$host}_NPM_USER"] ?? '',
|
|
'npm_pass' => $v["{$host}_NPM_PASS"] ?? '',
|
|
'lldap_url' => rtrim($v["{$host}_LLDAP_URL"] ?? 'http://localhost:17170', '/'),
|
|
'lldap_user' => $v["{$host}_LLDAP_USER"] ?? '',
|
|
'lldap_pass' => $v["{$host}_LLDAP_PASS"] ?? '',
|
|
'authelia_config' => $v["{$host}_AUTHELIA_CONFIG"] ?? '/mnt/user/appdata-Fallback/Critical-Data/Authelia/configuration.yml',
|
|
'authelia_container' => $v["{$host}_AUTHELIA_CONTAINER"] ?? 'Authelia',
|
|
'is_owner' => vv_is_owner(),
|
|
];
|
|
}
|
|
|
|
// ── NPM ───────────────────────────────────────────────────────────────────────
|
|
|
|
function vv_npm_token(): string {
|
|
if (!session_id()) session_start();
|
|
$conf = vv_auth_conf();
|
|
$cached = $_SESSION['vv_npm_token'] ?? '';
|
|
$expiry = $_SESSION['vv_npm_token_exp'] ?? 0;
|
|
if ($cached && time() < $expiry) return $cached;
|
|
|
|
$resp = vv_npm_raw('POST', '/api/tokens', [
|
|
'identity' => $conf['npm_user'],
|
|
'secret' => $conf['npm_pass'],
|
|
], '', $conf);
|
|
$token = $resp['token'] ?? '';
|
|
if ($token) {
|
|
$_SESSION['vv_npm_token'] = $token;
|
|
$_SESSION['vv_npm_token_exp'] = time() + 82800;
|
|
}
|
|
return $token;
|
|
}
|
|
|
|
function vv_npm_raw(string $method, string $path, array $data, string $token, array $conf = []): array {
|
|
if (!$conf) $conf = vv_auth_conf();
|
|
$url = $conf['npm_url'] . $path;
|
|
$headers = ['Content-Type: application/json', 'Accept: application/json'];
|
|
if ($token) $headers[] = 'Authorization: Bearer ' . $token;
|
|
|
|
$ch = curl_init($url);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => 10,
|
|
CURLOPT_HTTPHEADER => $headers,
|
|
CURLOPT_CUSTOMREQUEST => $method,
|
|
]);
|
|
if ($data && in_array($method, ['POST', 'PUT'], true))
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
|
$body = curl_exec($ch);
|
|
curl_close($ch);
|
|
return json_decode($body ?: '{}', true) ?: [];
|
|
}
|
|
|
|
function vv_npm_req(string $method, string $path, array $data = []): array {
|
|
$token = vv_npm_token();
|
|
if (!$token) return ['_err' => 'NPM auth failed — check credentials in host conf'];
|
|
return vv_npm_raw($method, $path, $data, $token);
|
|
}
|
|
|
|
function vv_npm_list_proxies(): array {
|
|
$list = vv_npm_req('GET', '/api/nginx/proxy-hosts?expand=certificate');
|
|
if (!is_array($list) || isset($list['_err']))
|
|
return ['ok' => false, 'error' => $list['_err'] ?? 'Invalid response from NPM'];
|
|
return ['ok' => true, 'proxies' => $list];
|
|
}
|
|
|
|
function vv_npm_list_certs(): array {
|
|
$list = vv_npm_req('GET', '/api/nginx/certificates');
|
|
return is_array($list) ? $list : [];
|
|
}
|
|
|
|
function vv_npm_create_proxy(array $data): array {
|
|
$r = vv_npm_req('POST', '/api/nginx/proxy-hosts', $data);
|
|
return isset($r['id']) ? ['ok' => true, 'proxy' => $r] : ['ok' => false, 'error' => $r['error'] ?? ($r['_err'] ?? 'Create failed')];
|
|
}
|
|
|
|
function vv_npm_update_proxy(int $id, array $data): array {
|
|
$r = vv_npm_req('PUT', "/api/nginx/proxy-hosts/$id", $data);
|
|
return isset($r['id']) ? ['ok' => true, 'proxy' => $r] : ['ok' => false, 'error' => $r['error'] ?? ($r['_err'] ?? 'Update failed')];
|
|
}
|
|
|
|
function vv_npm_delete_proxy(int $id): array {
|
|
vv_npm_req('DELETE', "/api/nginx/proxy-hosts/$id");
|
|
return ['ok' => true];
|
|
}
|
|
|
|
function vv_npm_toggle_proxy(int $id, bool $enabled): array {
|
|
vv_npm_req('POST', "/api/nginx/proxy-hosts/$id/" . ($enabled ? 'enable' : 'disable'));
|
|
return ['ok' => true];
|
|
}
|
|
|
|
// ── lldap ─────────────────────────────────────────────────────────────────────
|
|
|
|
function vv_lldap_token(): string {
|
|
if (!session_id()) session_start();
|
|
$conf = vv_auth_conf();
|
|
$cached = $_SESSION['vv_lldap_token'] ?? '';
|
|
$expiry = $_SESSION['vv_lldap_token_exp'] ?? 0;
|
|
if ($cached && time() < $expiry) return $cached;
|
|
|
|
$ch = curl_init($conf['lldap_url'] . '/auth/simple/login');
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => 10,
|
|
CURLOPT_POST => true,
|
|
CURLOPT_POSTFIELDS => json_encode(['username' => $conf['lldap_user'], 'password' => $conf['lldap_pass']]),
|
|
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
|
]);
|
|
$body = curl_exec($ch);
|
|
curl_close($ch);
|
|
$resp = json_decode($body ?: '{}', true) ?: [];
|
|
$token = $resp['token'] ?? '';
|
|
if ($token) {
|
|
$_SESSION['vv_lldap_token'] = $token;
|
|
$_SESSION['vv_lldap_token_exp'] = time() + 3500;
|
|
}
|
|
return $token;
|
|
}
|
|
|
|
function vv_lldap_gql(string $query, array $variables = []): array {
|
|
$conf = vv_auth_conf();
|
|
$token = vv_lldap_token();
|
|
if (!$token) return ['errors' => [['message' => 'lldap auth failed — check credentials']]];
|
|
|
|
$ch = curl_init($conf['lldap_url'] . '/api/graphql');
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => 10,
|
|
CURLOPT_POST => true,
|
|
CURLOPT_POSTFIELDS => json_encode(['query' => $query, 'variables' => $variables]),
|
|
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . $token],
|
|
]);
|
|
$body = curl_exec($ch);
|
|
curl_close($ch);
|
|
return json_decode($body ?: '{}', true) ?: [];
|
|
}
|
|
|
|
function vv_lldap_list_users(): array {
|
|
$r = vv_lldap_gql('query { users { id displayName email creationDate groups { id displayName } } }');
|
|
if (isset($r['errors'])) return ['ok' => false, 'error' => $r['errors'][0]['message'] ?? 'Query failed'];
|
|
return ['ok' => true, 'users' => $r['data']['users'] ?? []];
|
|
}
|
|
|
|
function vv_lldap_list_groups(): array {
|
|
$r = vv_lldap_gql('query { groups { id displayName users { id displayName } } }');
|
|
if (isset($r['errors'])) return ['ok' => false, 'error' => $r['errors'][0]['message'] ?? 'Query failed'];
|
|
return ['ok' => true, 'groups' => $r['data']['groups'] ?? []];
|
|
}
|
|
|
|
function vv_lldap_create_user(string $id, string $email, string $displayName, string $password): array {
|
|
$r = vv_lldap_gql(
|
|
'mutation CreateUser($user: CreateUserInput!) { createUser(user: $user) { id displayName email } }',
|
|
['user' => ['id' => $id, 'email' => $email, 'displayName' => $displayName]]
|
|
);
|
|
if (isset($r['errors'])) return ['ok' => false, 'error' => $r['errors'][0]['message'] ?? 'Create failed'];
|
|
if ($password) vv_lldap_set_password($id, $password);
|
|
return ['ok' => true, 'user' => $r['data']['createUser'] ?? []];
|
|
}
|
|
|
|
function vv_lldap_update_user(string $id, string $email, string $displayName): array {
|
|
$r = vv_lldap_gql(
|
|
'mutation UpdateUser($user: UpdateUserInput!) { updateUser(user: $user) { ok } }',
|
|
['user' => ['id' => $id, 'email' => $email, 'displayName' => $displayName]]
|
|
);
|
|
if (isset($r['errors'])) return ['ok' => false, 'error' => $r['errors'][0]['message'] ?? 'Update failed'];
|
|
return ['ok' => true];
|
|
}
|
|
|
|
function vv_lldap_delete_user(string $id): array {
|
|
$r = vv_lldap_gql(
|
|
'mutation DeleteUser($userId: String!) { deleteUser(userId: $userId) { ok } }',
|
|
['userId' => $id]
|
|
);
|
|
if (isset($r['errors'])) return ['ok' => false, 'error' => $r['errors'][0]['message'] ?? 'Delete failed'];
|
|
return ['ok' => true];
|
|
}
|
|
|
|
function vv_lldap_set_password(string $userId, string $password): array {
|
|
$conf = vv_auth_conf();
|
|
$token = vv_lldap_token();
|
|
if (!$token) return ['ok' => false, 'error' => 'lldap auth failed'];
|
|
|
|
$ch = curl_init($conf['lldap_url'] . '/auth/admin/resetPassword');
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => 10,
|
|
CURLOPT_POST => true,
|
|
CURLOPT_POSTFIELDS => json_encode(['userId' => $userId, 'password' => $password]),
|
|
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . $token],
|
|
]);
|
|
$body = curl_exec($ch);
|
|
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
if ($code >= 200 && $code < 300) return ['ok' => true];
|
|
$err = json_decode($body ?: '{}', true)['message'] ?? "HTTP $code";
|
|
return ['ok' => false, 'error' => $err];
|
|
}
|
|
|
|
function vv_lldap_create_group(string $name): array {
|
|
$r = vv_lldap_gql(
|
|
'mutation CreateGroup($name: String!) { createGroup(name: $name) { id displayName } }',
|
|
['name' => $name]
|
|
);
|
|
if (isset($r['errors'])) return ['ok' => false, 'error' => $r['errors'][0]['message'] ?? 'Create failed'];
|
|
return ['ok' => true, 'group' => $r['data']['createGroup'] ?? []];
|
|
}
|
|
|
|
function vv_lldap_delete_group(int $id): array {
|
|
$r = vv_lldap_gql(
|
|
'mutation DeleteGroup($groupId: Int!) { deleteGroup(groupId: $groupId) { ok } }',
|
|
['groupId' => $id]
|
|
);
|
|
if (isset($r['errors'])) return ['ok' => false, 'error' => $r['errors'][0]['message'] ?? 'Delete failed'];
|
|
return ['ok' => true];
|
|
}
|
|
|
|
function vv_lldap_add_to_group(string $userId, int $groupId): array {
|
|
$r = vv_lldap_gql(
|
|
'mutation AddUserToGroup($userId: String!, $groupId: Int!) { addUserToGroup(userId: $userId, groupId: $groupId) { ok } }',
|
|
['userId' => $userId, 'groupId' => $groupId]
|
|
);
|
|
if (isset($r['errors'])) return ['ok' => false, 'error' => $r['errors'][0]['message'] ?? 'Failed'];
|
|
return ['ok' => true];
|
|
}
|
|
|
|
function vv_lldap_remove_from_group(string $userId, int $groupId): array {
|
|
$r = vv_lldap_gql(
|
|
'mutation RemoveUserFromGroup($userId: String!, $groupId: Int!) { removeUserFromGroup(userId: $userId, groupId: $groupId) { ok } }',
|
|
['userId' => $userId, 'groupId' => $groupId]
|
|
);
|
|
if (isset($r['errors'])) return ['ok' => false, 'error' => $r['errors'][0]['message'] ?? 'Failed'];
|
|
return ['ok' => true];
|
|
}
|
|
|
|
// ── Authelia ──────────────────────────────────────────────────────────────────
|
|
|
|
function vv_authelia_read_rules(): array {
|
|
$conf = vv_auth_conf();
|
|
$file = $conf['authelia_config'];
|
|
if (!file_exists($file)) return ['ok' => false, 'error' => 'Config not found: ' . $file];
|
|
|
|
$content = file_get_contents($file);
|
|
if ($content === false) return ['ok' => false, 'error' => 'Cannot read config file'];
|
|
|
|
// Extract default_policy (strip inline comments)
|
|
$defaultPolicy = 'deny';
|
|
if (preg_match('/^[ \t]+default_policy:[ \t]+([a-z_]+)/m', $content, $m))
|
|
$defaultPolicy = $m[1];
|
|
|
|
// Extract the indented block under access_control:
|
|
if (!preg_match('/^access_control:[ \t]*\n((?:[ \t][^\n]*\n?)*)/m', $content, $m))
|
|
return ['ok' => false, 'error' => 'access_control section not found'];
|
|
|
|
$acBlock = $m[1];
|
|
|
|
// Extract the indented block under rules: (3+ space indent = rule list items)
|
|
if (!preg_match('/^ rules:[ \t]*\n((?:[ \t]{3,}[^\n]*\n?)*)/m', $acBlock, $m))
|
|
return ['ok' => true, 'default_policy' => $defaultPolicy, 'rules' => []];
|
|
|
|
// Split into individual rule chunks at " - " (indent-4 rule starts)
|
|
$chunks = preg_split('/(?=^ - )/m', $m[1]);
|
|
$rules = [];
|
|
foreach ($chunks as $chunk) {
|
|
if (!preg_match('/^ - /', $chunk)) continue;
|
|
$rule = vv_authelia_parse_rule_chunk($chunk);
|
|
if (!empty($rule)) $rules[] = $rule;
|
|
}
|
|
|
|
return ['ok' => true, 'default_policy' => $defaultPolicy, 'rules' => $rules];
|
|
}
|
|
|
|
function vv_authelia_parse_rule_chunk(string $chunk): array {
|
|
$rule = [];
|
|
$field = null;
|
|
$list = [];
|
|
|
|
$save = function () use (&$rule, &$field, &$list) {
|
|
if ($field === null) return;
|
|
if (!empty($list))
|
|
$rule[$field] = count($list) === 1 ? $list[0] : $list;
|
|
$field = null;
|
|
$list = [];
|
|
};
|
|
|
|
foreach (explode("\n", $chunk) as $line) {
|
|
$raw = rtrim($line);
|
|
$trim = trim($raw);
|
|
if ($trim === '' || preg_match('/^#+/', $trim)) continue;
|
|
$indent = strlen($raw) - strlen(ltrim($raw, ' '));
|
|
|
|
// indent=4, starts with "- " → first field of this rule block
|
|
if ($indent === 4 && str_starts_with($trim, '- ')) {
|
|
$rest = ltrim(substr($trim, 2));
|
|
if (preg_match('/^([a-z_]+):[ \t]*(.*)$/', $rest, $m)) {
|
|
$save();
|
|
$field = $m[1];
|
|
$val = trim($m[2]);
|
|
if ($val !== '' && !str_starts_with($val, '#')) {
|
|
$rule[$field] = vv_authelia_unquote($val);
|
|
$field = null;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// indent=6 → named field (scalar or list header)
|
|
if ($indent === 6 && preg_match('/^([a-z_]+):[ \t]*(.*)$/', $trim, $m)) {
|
|
$save();
|
|
$field = $m[1];
|
|
$val = trim($m[2]);
|
|
if ($val !== '' && !str_starts_with($val, '#')) {
|
|
$rule[$field] = vv_authelia_unquote($val);
|
|
$field = null;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// indent=8, starts with "- " → list item under current field
|
|
if ($indent === 8 && str_starts_with($trim, '- ')) {
|
|
$list[] = vv_authelia_parse_list_item(trim(substr($trim, 2)));
|
|
}
|
|
}
|
|
$save();
|
|
return $rule;
|
|
}
|
|
|
|
// Strip surrounding quotes and inline comments from a YAML scalar.
|
|
function vv_authelia_unquote(string $val): string {
|
|
$val = trim($val);
|
|
$val = preg_replace('/\s+#[^"\']*$/', '', $val); // strip trailing comment
|
|
if (preg_match('/^(["\'])(.+)\1$/', $val, $m)) return $m[2];
|
|
return $val;
|
|
}
|
|
|
|
// Parse a YAML list item: flow sequence ['group:name'] or plain/quoted scalar.
|
|
function vv_authelia_parse_list_item(string $val): string {
|
|
$val = trim($val);
|
|
// Flow sequence: ['value'] or ["value"] or [value]
|
|
if (preg_match('/^\[[\'""]?([^\]\'""]+)[\'""]?\]$/', $val, $m)) return trim($m[1]);
|
|
return vv_authelia_unquote($val);
|
|
}
|
|
|
|
function vv_authelia_write_rules(array $rules, string $defaultPolicy): array {
|
|
$conf = vv_auth_conf();
|
|
$file = $conf['authelia_config'];
|
|
if (!file_exists($file)) return ['ok' => false, 'error' => 'Config not found: ' . $file];
|
|
|
|
$content = file_get_contents($file);
|
|
if ($content === false) return ['ok' => false, 'error' => 'Cannot read config file'];
|
|
|
|
// Build the new access_control block
|
|
$block = "access_control:\n";
|
|
$block .= " default_policy: $defaultPolicy\n";
|
|
$block .= " rules:\n";
|
|
|
|
// Preferred field output order
|
|
$fieldOrder = ['domain', 'policy', 'subject', 'networks', 'resources'];
|
|
|
|
foreach ($rules as $rule) {
|
|
$keys = array_merge(
|
|
array_filter($fieldOrder, fn($k) => array_key_exists($k, $rule)),
|
|
array_diff(array_keys($rule), $fieldOrder)
|
|
);
|
|
$first = true;
|
|
foreach ($keys as $key) {
|
|
if (!array_key_exists($key, $rule)) continue;
|
|
$val = $rule[$key];
|
|
$prefix = $first ? ' - ' : ' ';
|
|
$first = false;
|
|
|
|
// domain, subject, resources, networks → always output as list
|
|
$isList = in_array($key, ['domain', 'subject', 'resources', 'networks'], true);
|
|
if ($isList) {
|
|
$items = is_array($val) ? $val : [$val];
|
|
$block .= $prefix . $key . ":\n";
|
|
foreach ($items as $item) {
|
|
$out = $key === 'subject'
|
|
? "['" . $item . "']"
|
|
: vv_authelia_yaml_scalar((string) $item);
|
|
$block .= ' - ' . $out . "\n";
|
|
}
|
|
} else {
|
|
$block .= $prefix . $key . ': ' . vv_authelia_yaml_scalar((string) $val) . "\n";
|
|
}
|
|
}
|
|
}
|
|
|
|
// Replace existing access_control: block (from its line to next top-level key or EOF)
|
|
$pattern = '/^access_control:[ \t]*\n(?:[ \t][^\n]*\n?)*/m';
|
|
$new = preg_match($pattern, $content)
|
|
? preg_replace($pattern, $block, $content, 1)
|
|
: rtrim($content) . "\n\n" . $block;
|
|
|
|
if ($new === null) return ['ok' => false, 'error' => 'Regex replace failed'];
|
|
|
|
$tmp = $file . '.vv.tmp';
|
|
if (file_put_contents($tmp, $new) === false) return ['ok' => false, 'error' => 'Write failed'];
|
|
if (!rename($tmp, $file)) { @unlink($tmp); return ['ok' => false, 'error' => 'Atomic rename failed']; }
|
|
|
|
shell_exec('docker restart ' . escapeshellarg($conf['authelia_container']) . ' >/dev/null 2>&1 &');
|
|
return ['ok' => true];
|
|
}
|
|
|
|
// Quote a YAML scalar value if it contains characters that require quoting.
|
|
function vv_authelia_yaml_scalar(string $val): string {
|
|
if ($val === '' || preg_match('/[:#\[\]{},|>&*?!%@`\'"]/', $val) || preg_match('/^\s|\s$/', $val))
|
|
return '"' . str_replace(['\\', '"'], ['\\\\', '\\"'], $val) . '"';
|
|
return $val;
|
|
}
|