Plan: audit entries kept forever unless config sets retention_days

88a5fab · Gabriel Morell · 2026-10-20 21:12

3 files +20 -11

Files changed

modified docs/plans/org-audit-log.md
+20 −11
@@ -1,6 +1,6 @@
1 1 # Plan: an audit log for organizations, repositories and the instance
2 2
3 −Status: **draft — decisions recorded below (2026-10-20); retention still to confirm.**
3 +Status: **ready to execute** — decisions recorded below (2026-10-20); the remaining open questions have recommended defaults.
4 4 Independent of the CI plans. Supersedes the log lines added in 77def2d
5 5 ("Secrets: changing a secret or variable leaves a line in the log") and
6 6 closes item 12 of `security-review-ci.md` ("Missing audit logging for
@@ -46,7 +46,7 @@ made this repo public / registered this runner?" Today:
46 46 | Scope | `lib/git_gud/accounts/scope.ex`, built in `UserAuth.fetch_current_scope_for_user/2` and `mount_current_scope/2` |
47 47 | Settings nav | `org_settings_header/1`, `repo_settings_header/1` in `lib/git_gud_web/components/core_components.ex`; org settings links in `lib/git_gud_web/live/org_live/show.ex` |
48 48 | Router | org and repo settings pages live in `live_session :authenticated_repo` |
49 −| Retention precedent | `GitGud.Federation.Workers.Retention`; cron in `config/config.exs`; admin-editable knobs in `GitGud.Settings` (`@keys`) |
49 +| Retention precedent | `GitGud.Federation.Workers.Retention`; cron in `config/config.exs` |
50 50 | Pagination precedent | `OrgLive.Index` (`/orgs/page/:page`, `page_window/2`) |
51 51 | Mobile API auth | `GitGudWeb.Api.Mobile.Auth.require_scope/2`; scopes in `PersonalAccessToken` `@scopes` |
52 52 | CLI | `cli/src/commands/*.rs`, speaks `/api/mobile/v1` (`cli/README.md`: parity with Android) |
@@ -347,10 +347,19 @@ Every view needs sudo mode; IP and user agent show only in the admin view.
347 347
348 348 ## 5. Retention
349 349
350 +Kept forever by default. The window is application config, set in
351 +`config/config.exs` (or a deployment's own config):
352 +
353 +```elixir
354 +# nil keeps every entry; an integer N prunes entries older than N days.
355 +config :git_gud, GitGud.Audit, retention_days: nil
356 +```
357 +
350 358 `GitGud.Audit.Workers.Prune` (`queue: :default`), `@daily` in the
351 −`config/config.exs` crontab. The window is a `GitGud.Settings` key
352 −`audit.retention_days` (`type: :integer, default: nil, min: 30, nullable: true`),
353 −editable by an admin without a deploy. `nil` keeps everything. Deletes in
359 +`config/config.exs` crontab, reads it at run time and does nothing while
360 +it is `nil`. Boot refuses a value that isn't `nil` or a positive integer.
361 +Not an admin-editable `GitGud.Settings` key: shortening the audit trail
362 +shouldn't be something a compromised admin session can do. Deletes in
354 363 batches of 5 000 by id
355 364 (`DELETE … WHERE id IN (SELECT id … WHERE inserted_at < $1 LIMIT 5000)`)
356 365 until a batch comes back short, so no long lock, and records one
@@ -418,7 +427,7 @@ Same table: `organization_id`/`repository_id` nil, `subject_user_id` and
418 427 repo's entries render with the snapshot label.
419 428 - `audit_live/repo_test.exs`, `audit_live/admin_test.exs`: the same
420 429 permission matrix.
421 −- `test/git_gud/audit/workers/prune_test.exs`: nil keeps everything; N days
430 +- `test/git_gud/audit/workers/prune_test.exs`: `retention_days: nil` keeps everything; N days
422 431 deletes only older rows, across more than one batch; writes `audit.pruned`.
423 432 - API: without `audit:read` → 403; non-admin → 403; invisible org → 404;
424 433 `next_before`; no `ip` in the payload.
@@ -427,8 +436,8 @@ Same table: `organization_id`/`repository_id` nil, `subject_user_id` and
427 436
428 437 1. **Substrate.** Migration, `Audit.Entry`, `Audit.Catalog`, `Audit`
429 438 (`transact`, `insert!`, `multi`, `list`), `Audit.Actor`, Scope
430 − `ip`/`user_agent`, `:user_agent` connect_info, prune worker and
431 − `audit.retention_days`. No callers, no UI.
439 + `ip`/`user_agent`, `:user_agent` connect_info, prune worker and the
440 + `retention_days` config. No callers, no UI.
432 441 2. **Secrets and the org page.** Both `SecretsLive` modules (drop `audit/4`;
433 442 `Audit`'s log line replaces it), `AuditLive.Org` with category tabs and
434 443 keyset paging, the nav entry, `AuditComponents`. Closes review item #12.
@@ -460,9 +469,9 @@ Same table: `organization_id`/`repository_id` nil, `subject_user_id` and
460 469
461 470 - **Log line per entry: yes.** `Audit` keeps one `Logger.notice` per row
462 471 after commit (open question 1).
463 −- **Retention:** answered "yes" to "forever or 365 days" — to confirm
464 − which. Until then slice 1 ships the `audit.retention_days` setting with
465 − `nil` (keep everything) as the default, so nothing is pruned on a guess.
472 +- **Retention: forever by default,** configurable to N days with
473 + `config :git_gud, GitGud.Audit, retention_days: N` in `config.exs`
474 + (section 5; open question 2).
466 475 - **IP and user agent: instance admins only.** Org and repo views, and the
467 476 API, never show them; `/admin/audit` does (open question 3).
468 477 - **Viewing the log requires sudo mode.** All three views mount with

Parents: 36965b5