Make access-control domains an editable list, and stop saves eating the rule labels

The block is rebuilt from the parsed model on every save and the parser discarded comments,
so one press of Save deleted the five ## lines that are the only thing in the file saying
what each rule is for — which inline editing was about to make far easier to press.
This commit is contained in:
Gmer4Lfe
2026-08-15 11:30:36 -04:00
parent b22d034933
commit 693ac693a4
3 changed files with 227 additions and 33 deletions
+62 -13
View File
@@ -390,10 +390,16 @@ function vv_authelia_read_rules(): array {
$content = file_get_contents($file);
if ($content === false) return ['ok' => false, 'error' => 'Cannot read config file'];
// Extract default_policy (strip inline comments)
// Extract default_policy, and keep any trailing comment rather than dropping it. The line in
// this config reads "default_policy: bypass #deny" — a note about what it used to be, or is
// meant to become, on the single most consequential setting in the file. The block is
// re-emitted on save, so anything not carried here is deleted by the next save.
$defaultPolicy = 'deny';
if (preg_match('/^[ \t]+default_policy:[ \t]+([a-z_]+)/m', $content, $m))
$defaultNote = '';
if (preg_match('/^[ \t]+default_policy:[ \t]+([a-z_]+)[ \t]*(#[^\n]*)?/m', $content, $m)) {
$defaultPolicy = $m[1];
$defaultNote = trim($m[2] ?? '');
}
// Extract the indented block under access_control:
if (!preg_match('/^access_control:[ \t]*\n((?:[ \t][^\n]*\n?)*)/m', $content, $m))
@@ -403,18 +409,46 @@ function vv_authelia_read_rules(): array {
// 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' => []];
return ['ok' => true, 'default_policy' => $defaultPolicy, 'rules' => [],
'default_note' => $defaultNote];
// 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;
// Walked rather than preg_split, so the comment lines above each rule can be attached to it.
//
// This block is rebuilt from the parsed model on every save, so anything the parser drops is
// deleted the next time anyone touches this page — and the parser dropped every comment. The
// five rules here are labelled ## Media_Users_users, ## Admin Only, ## super_users,
// ## power_users and ## Home_users, which is the only thing in the file that says what a rule
// is *for*: the rule itself is thirteen hostnames and a group id. Saving once erased all five.
//
// A lookahead split cannot do this, because the comment above rule N lands at the end of rule
// N-1's chunk (or before the first chunk entirely), so it would be attributed to the wrong
// rule or lost with the preamble.
$lines = explode("\n", $m[1]);
$starts = [];
foreach ($lines as $i => $l) if (preg_match('/^ - /', $l)) $starts[] = $i;
$rules = [];
foreach ($starts as $n => $s) {
// Contiguous comment lines immediately above this rule, in file order. A blank line or
// any content ends the run — a comment separated from the rule by a blank belongs to the
// block, not to the rule.
$label = [];
for ($j = $s - 1; $j >= 0; $j--) {
if (!preg_match('/^\s*#/', $lines[$j])) break;
array_unshift($label, trim($lines[$j]));
}
$end = $starts[$n + 1] ?? count($lines);
$chunk = implode("\n", array_slice($lines, $s, $end - $s));
$rule = vv_authelia_parse_rule_chunk($chunk);
if (empty($rule)) continue;
// Underscore-prefixed so it cannot collide with an Authelia field name, and so the writer
// can tell presentation from configuration when it decides what to emit as YAML.
if ($label) $rule['_label'] = $label;
$rules[] = $rule;
}
return ['ok' => true, 'default_policy' => $defaultPolicy, 'rules' => $rules];
return ['ok' => true, 'default_policy' => $defaultPolicy, 'rules' => $rules,
'default_note' => $defaultNote];
}
function vv_authelia_parse_rule_chunk(string $chunk): array {
@@ -488,7 +522,7 @@ function vv_authelia_parse_list_item(string $val): string {
return vv_authelia_unquote($val);
}
function vv_authelia_write_rules(array $rules, string $defaultPolicy): array {
function vv_authelia_write_rules(array $rules, string $defaultPolicy, string $defaultNote = ''): array {
$conf = vv_auth_conf();
$file = $conf['authelia_config'];
if (!file_exists($file)) return ['ok' => false, 'error' => 'Config not found: ' . $file];
@@ -498,17 +532,32 @@ function vv_authelia_write_rules(array $rules, string $defaultPolicy): array {
// Build the new access_control block
$block = "access_control:\n";
$block .= " default_policy: $defaultPolicy\n";
$block .= " default_policy: $defaultPolicy" . ($defaultNote !== '' ? ' ' . $defaultNote : '') . "\n";
$block .= " rules:\n";
// Preferred field output order
$fieldOrder = ['domain', 'policy', 'subject', 'networks', 'resources'];
foreach ($rules as $rule) {
// The labels the operator wrote above this rule, put back before it. Emitted here rather
// than inside the field loop because they are not a field — they carry no indent-4 dash
// and must land above the rule, not inside it.
foreach ((array) ($rule['_label'] ?? []) as $lbl) {
$lbl = trim((string) $lbl);
if ($lbl === '') continue;
// Forced back into comment form. This string reaches here from the browser, and a
// label that lost its # would be spliced into the config as YAML.
if ($lbl[0] !== '#') $lbl = '# ' . $lbl;
// One line only — a newline here would end the comment and start config.
$block .= ' ' . str_replace(["\r", "\n"], ' ', $lbl) . "\n";
}
$keys = array_merge(
array_filter($fieldOrder, fn($k) => array_key_exists($k, $rule)),
array_diff(array_keys($rule), $fieldOrder)
);
// Presentation, already emitted above. Left in the key list it would be written out as a
// YAML field named _label, which Authelia would reject on load.
$keys = array_filter($keys, fn($k) => $k !== '_label');
$first = true;
foreach ($keys as $key) {
if (!array_key_exists($key, $rule)) continue;