Auth page: pure-PHP Authelia ACL parser, NPM-sourced certs tab, watchdog JS fixes, rsync settings layout
- Replace python3/PyYAML Authelia ACL parser with pure PHP (no deps available on Unraid) - Cert tab now pulls live from NPM API instead of cert_monitor.sh — auto-discovers all managed certs sorted by urgency - Watchdog page: add missing GB constant and _fmtBytes/_relTime functions that were causing silent render failure - Rsync settings card: pin to far-right 3 columns (grid-column:6/-1), toggle grid narrowed to 2 columns - Add CLAUDE.md project context file on /boot for session persistence across reboots - claude_startup.sh: symlink CLAUDE.md into /root on array start
This commit is contained in:
+178
-45
@@ -31,7 +31,6 @@ function vv_npm_token(): string {
|
||||
$resp = vv_npm_raw('POST', '/api/tokens', [
|
||||
'identity' => $conf['npm_user'],
|
||||
'secret' => $conf['npm_pass'],
|
||||
'expiry' => '1d',
|
||||
], '', $conf);
|
||||
$token = $resp['token'] ?? '';
|
||||
if ($token) {
|
||||
@@ -146,15 +145,15 @@ function vv_lldap_gql(string $query, array $variables = []): array {
|
||||
}
|
||||
|
||||
function vv_lldap_list_users(): array {
|
||||
$r = vv_lldap_gql('query { listUsers { id displayName email creationDate groups { id displayName } } }');
|
||||
$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']['listUsers'] ?? []];
|
||||
return ['ok' => true, 'users' => $r['data']['users'] ?? []];
|
||||
}
|
||||
|
||||
function vv_lldap_list_groups(): array {
|
||||
$r = vv_lldap_gql('query { listGroups { id displayName users { id displayName } } }');
|
||||
$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']['listGroups'] ?? []];
|
||||
return ['ok' => true, 'groups' => $r['data']['groups'] ?? []];
|
||||
}
|
||||
|
||||
function vv_lldap_create_user(string $id, string $email, string $displayName, string $password): array {
|
||||
@@ -186,12 +185,24 @@ function vv_lldap_delete_user(string $id): array {
|
||||
}
|
||||
|
||||
function vv_lldap_set_password(string $userId, string $password): array {
|
||||
$r = vv_lldap_gql(
|
||||
'mutation ChangePassword($userId: String!, $password: String!) { changeUserPassword(userId: $userId, password: $password) }',
|
||||
['userId' => $userId, 'password' => $password]
|
||||
);
|
||||
if (isset($r['errors'])) return ['ok' => false, 'error' => $r['errors'][0]['message'] ?? 'Password change failed'];
|
||||
return ['ok' => true];
|
||||
$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 {
|
||||
@@ -237,19 +248,105 @@ function vv_authelia_read_rules(): array {
|
||||
$file = $conf['authelia_config'];
|
||||
if (!file_exists($file)) return ['ok' => false, 'error' => 'Config not found: ' . $file];
|
||||
|
||||
$py = "import yaml,json,sys\n"
|
||||
. "d=yaml.safe_load(open(sys.argv[1]))\n"
|
||||
. "ac=d.get('access_control',{})\n"
|
||||
. "print(json.dumps({'default_policy':ac.get('default_policy','deny'),'rules':ac.get('rules',[])}))\n";
|
||||
$tmp = '/tmp/vv_auth_rd_' . getmypid() . '.py';
|
||||
file_put_contents($tmp, $py);
|
||||
$out = shell_exec('python3 ' . escapeshellarg($tmp) . ' ' . escapeshellarg($file) . ' 2>/dev/null');
|
||||
@unlink($tmp);
|
||||
$content = file_get_contents($file);
|
||||
if ($content === false) return ['ok' => false, 'error' => 'Cannot read config file'];
|
||||
|
||||
if (!$out) return ['ok' => false, 'error' => 'Parse failed — python3 with PyYAML required'];
|
||||
$data = json_decode(trim($out), true);
|
||||
if (!$data) return ['ok' => false, 'error' => 'Invalid YAML response'];
|
||||
return ['ok' => true, 'default_policy' => $data['default_policy'], 'rules' => $data['rules']];
|
||||
// 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 {
|
||||
@@ -257,29 +354,65 @@ function vv_authelia_write_rules(array $rules, string $defaultPolicy): array {
|
||||
$file = $conf['authelia_config'];
|
||||
if (!file_exists($file)) return ['ok' => false, 'error' => 'Config not found: ' . $file];
|
||||
|
||||
$acJson = json_encode(['default_policy' => $defaultPolicy, 'rules' => $rules]);
|
||||
$py = <<<'PYEOF'
|
||||
import yaml, json, sys, re
|
||||
config_file = sys.argv[1]
|
||||
new_ac = json.loads(sys.argv[2])
|
||||
with open(config_file, 'r') as f:
|
||||
content = f.read()
|
||||
new_block = yaml.dump({'access_control': new_ac}, default_flow_style=False, allow_unicode=True, sort_keys=False)
|
||||
pattern = r'(?ms)^access_control:.*?(?=^[a-zA-Z#]|\Z)'
|
||||
if re.search(pattern, content):
|
||||
content = re.sub(pattern, new_block + '\n', content)
|
||||
else:
|
||||
content = content.rstrip('\n') + '\n\n' + new_block + '\n'
|
||||
with open(config_file, 'w') as f:
|
||||
f.write(content)
|
||||
print('ok')
|
||||
PYEOF;
|
||||
$tmp = '/tmp/vv_auth_wr_' . getmypid() . '.py';
|
||||
file_put_contents($tmp, $py);
|
||||
$out = shell_exec('python3 ' . escapeshellarg($tmp) . ' ' . escapeshellarg($file) . ' ' . escapeshellarg($acJson) . ' 2>&1');
|
||||
@unlink($tmp);
|
||||
$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']; }
|
||||
|
||||
if (trim($out) !== 'ok') return ['ok' => false, 'error' => 'Write failed: ' . trim($out)];
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user