Add per-agent log retention floor, owner-only to set or override
api/agents.ConfigOverride gains LogRetentionDays: a per-agent setting edited on the same remote-config page as extra_file_paths, but unlike every other field there it's central-policy metadata api/logretention reads, never something the agent process itself sees. Any change to it -- setting, raising, lowering, or clearing -- requires RoleOwner, not just RoleAdmin: the whole point of the field is a floor an admin can't move, so an admin able to freely edit it would defeat that. api/logretention now checks the largest LogRetentionDays configured across any agent (AgentRetentionStore, new) before every preview/delete: a non-owner's request is rejected with a clear 403 if it would reach into that protected window. An owner always bypasses it, matching "make the log retention override any attempts to delete logs by anyone other than owner role." Verified live end-to-end: owner sets a 90-day floor on an agent, admin is blocked deleting anything newer than that (both preview and delete), allowed beyond it, and owner bypasses it entirely -- confirmed against real ClickHouse data, not just the fake-backed unit tests. Also caught and fixed a real pre-existing latent bug while verifying in-browser: a type="number" Input's bind:value becomes an actual JS number once a user types into it (only the initial value is a string), which broke a bare .trim() call on the new field.
This commit is contained in:
@@ -590,6 +590,11 @@ export type ConfigOverride = {
|
||||
heartbeat_interval_ms?: number;
|
||||
journald_unit?: string;
|
||||
extra_file_paths?: string[];
|
||||
// log_retention_days is owner-only to change (see api/agents/handler.go's
|
||||
// changesLogRetentionDays) -- it's a central policy tag api/logretention
|
||||
// reads as a protective floor, not something the agent process itself
|
||||
// ever sees or applies.
|
||||
log_retention_days?: number;
|
||||
};
|
||||
|
||||
export type Agent = {
|
||||
|
||||
@@ -33,6 +33,11 @@
|
||||
// an agent with no override starts with an empty list, not
|
||||
// something derived from `agent`.
|
||||
let extraFilePaths = $state<string[]>([]);
|
||||
// Same "no effective value to fall back to" shape as extra_file_paths
|
||||
// -- and like that field, the agent process itself never reads this
|
||||
// one either. Kept as a string ('' = no override) so the input can be
|
||||
// empty rather than defaulting to some arbitrary number of days.
|
||||
let logRetentionDays = $state('');
|
||||
|
||||
function resetForm(a: Agent) {
|
||||
const o = a.desired_override;
|
||||
@@ -42,6 +47,7 @@
|
||||
heartbeatIntervalMs = String(o?.heartbeat_interval_ms ?? a.heartbeat_interval_ms);
|
||||
journaldUnit = o?.journald_unit ?? '';
|
||||
extraFilePaths = o?.extra_file_paths ? [...o.extra_file_paths] : [];
|
||||
logRetentionDays = o?.log_retention_days != null ? String(o.log_retention_days) : '';
|
||||
}
|
||||
|
||||
function addExtraFilePath() {
|
||||
@@ -76,7 +82,16 @@
|
||||
heartbeat_enabled: heartbeatEnabled,
|
||||
heartbeat_interval_ms: Number(heartbeatIntervalMs),
|
||||
...(agent?.source_kind === 'journald' ? { journald_unit: journaldUnit } : {}),
|
||||
extra_file_paths: extraFilePaths.map((p) => p.trim()).filter((p) => p !== '')
|
||||
extra_file_paths: extraFilePaths.map((p) => p.trim()).filter((p) => p !== ''),
|
||||
// String(...) first, deliberately -- Input's `value` prop is
|
||||
// declared string, but a type="number" input's bind:value
|
||||
// actually hands back a real JS number once the user has
|
||||
// typed into it (only the initial resetForm-assigned value is
|
||||
// guaranteed to be a string), so a bare .trim() here throws
|
||||
// the moment someone edits this field. batch_max_size etc.
|
||||
// above never hit this because Number(x) doesn't care
|
||||
// whether x is already a number.
|
||||
...(String(logRetentionDays).trim() !== '' ? { log_retention_days: Number(logRetentionDays) } : {})
|
||||
});
|
||||
} catch (e) {
|
||||
saveError = e instanceof Error ? e.message : String(e);
|
||||
@@ -211,6 +226,22 @@
|
||||
<Button variant="secondary" onclick={addExtraFilePath}>Add path</Button>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="log-retention-days">Log retention (days)</label>
|
||||
<Input
|
||||
id="log-retention-days"
|
||||
type="number"
|
||||
min="1"
|
||||
max="3650"
|
||||
placeholder="(empty = no protected minimum)"
|
||||
bind:value={logRetentionDays}
|
||||
/>
|
||||
<p class="hint">
|
||||
Owner only. Once set, this host's logs can't be deleted by age (Settings → Log retention) by anyone
|
||||
other than an owner until they're at least this many days old.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if saveError}<p class="error">Error: {saveError}</p>{/if}
|
||||
|
||||
<div class="actions">
|
||||
|
||||
Reference in New Issue
Block a user