vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
| 1 | package web |
| 2 | |
| 3 | // The audit log. Vole records every privileged action in one tab-separated |
| 4 | // shape, so one file answers "who did this" whether the change arrived over SSH, |
| 5 | // from the CLI, or from a form. internal/audit owns the shape and the escaping; |
| 6 | // this file is only the web's way in. |
| 7 | // |
| 8 | // A write failure is reported to the server log at error level and nothing else: |
| 9 | // the action has already happened, and refusing the response would not undo it. |
| 10 | // The acts that must fail closed — account, key and ACL changes, and a repository |
| 11 | // deletion — are recorded before they happen by the store they go through |
| 12 | // (forge/identity, forge/access, forge/repo), so a request that could not be |
| 13 | // recorded never reaches this file's callers. |
| 14 | |
| 15 | import ( |
| 16 | "rodent.software/vole/internal/audit" |
| 17 | ) |
| 18 | |
| 19 | // audit appends one record for an action that has already been taken. A failure |
| 20 | // is loud, never silent, and never changes the answer the browser already earned. |
| 21 | func (w *Web) audit(action, actor, subject, detail string) { |
| 22 | path := "" |
| 23 | if w.Cfg != nil { |
| 24 | path = w.Cfg.AuditLog() |
| 25 | } |
| 26 | if err := audit.Append(path, audit.Record{ |
| 27 | Action: action, |
| 28 | Actor: actor, |
| 29 | Subject: subject, |
| 30 | Detail: detail, |
| 31 | }); err != nil { |
| 32 | if w.Logger != nil { |
| 33 | w.Logger("audit log write failed: action=%s account=%s subject=%s: %v", |
| 34 | action, actor, subject, err) |
| 35 | } |
| 36 | } |
| 37 | // A browser write commits to the metadata branch directly, without a push for the |
| 38 | // post-receive hook to observe. Nudging the event loop after every audited write |
| 39 | // is cheap: it compares a cursor and does nothing when no metadata changed. |
| 40 | if w.MetadataChanged != nil { |
| 41 | w.MetadataChanged(subject) |
| 42 | } |
| 43 | } |
| 44 |