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.
47 lines
1.7 KiB
Go
47 lines
1.7 KiB
Go
package logretention
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
// AgentRetentionStore reads the protective retention floor set on
|
|
// agents.ConfigOverride.LogRetentionDays -- a separate, Postgres-backed
|
|
// concern from Store's ClickHouse access above, so it lives in its own
|
|
// file. Deliberately its own narrow query against the same `agents`
|
|
// table api/agents.Store manages, rather than importing api/agents for
|
|
// a shared type, matching this codebase's "each package owns direct
|
|
// SQL access to what it needs" convention (e.g. alerting and api both
|
|
// read dashboards-adjacent tables independently rather than sharing a
|
|
// store type across a package boundary).
|
|
type AgentRetentionStore struct {
|
|
pool *pgxpool.Pool
|
|
}
|
|
|
|
func NewAgentRetentionStore(pool *pgxpool.Pool) *AgentRetentionStore {
|
|
return &AgentRetentionStore{pool: pool}
|
|
}
|
|
|
|
// MaxRetentionDays reports the largest log_retention_days configured
|
|
// across any agent's desired_override, if any are set at all -- this is
|
|
// the floor a non-owner's deletion request must not reach into (see
|
|
// Handler.checkRetentionFloor). The second return value is false when
|
|
// no agent has this field configured, distinct from a configured floor
|
|
// of 0 (which validateOverride never allows to be stored in the first
|
|
// place).
|
|
func (s *AgentRetentionStore) MaxRetentionDays(ctx context.Context) (int, bool, error) {
|
|
var days *int
|
|
err := s.pool.QueryRow(ctx, `
|
|
SELECT max((desired_override->>'log_retention_days')::int)
|
|
FROM agents
|
|
WHERE desired_override->>'log_retention_days' IS NOT NULL`).Scan(&days)
|
|
if err != nil {
|
|
return 0, false, err
|
|
}
|
|
if days == nil {
|
|
return 0, false, nil
|
|
}
|
|
return *days, true, nil
|
|
}
|