mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-11 21:39:01 +00:00
05a9665f50
One-time bootstrap token generated on first start with no users in self-host mode. Token is logged in a banner the operator can grab from `docker logs`, persists at <DataDir>/.bootstrap-token (mode 0600), and bypasses the loopback-only gate via the X-Bootstrap-Token header — letting the user claim the first admin from a remote browser at /setup#token=<x>. Header-only contract + URL-fragment (browser-only, never transmitted) + log-redaction middleware keeps the secret out of access logs, proxy logs, and browser history. Cloud mode unchanged: token never loaded, never honored. Validate → UserCount-check → CreateUser → consume sequence is mutex-serialized to prevent concurrent valid-token requests from creating multiple admins. Part of PLAN-1166 (Pad on Unraid — Community Apps launch).
92 lines
1.9 KiB
Go
92 lines
1.9 KiB
Go
package server
|
|
|
|
import "testing"
|
|
|
|
// TestRedactQueryString covers the F6 leak guard: sensitive keys (token,
|
|
// password, secret, api_key / api-key) must have their values replaced
|
|
// with REDACTED before the structured request logger writes them to
|
|
// disk. Other keys must be untouched so logs remain useful for
|
|
// diagnosis.
|
|
func TestRedactQueryString(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
in string
|
|
want string
|
|
}{
|
|
{
|
|
name: "empty",
|
|
in: "",
|
|
want: "",
|
|
},
|
|
{
|
|
name: "no sensitive keys",
|
|
in: "ref=TASK-5&limit=20",
|
|
want: "ref=TASK-5&limit=20",
|
|
},
|
|
{
|
|
name: "token alone",
|
|
in: "token=abc123",
|
|
want: "token=REDACTED",
|
|
},
|
|
{
|
|
name: "token mid-string",
|
|
in: "ref=TASK-5&token=abc123&limit=20",
|
|
want: "ref=TASK-5&token=REDACTED&limit=20",
|
|
},
|
|
{
|
|
name: "token at end",
|
|
in: "ref=TASK-5&token=abc123",
|
|
want: "ref=TASK-5&token=REDACTED",
|
|
},
|
|
{
|
|
name: "case-insensitive",
|
|
in: "TOKEN=abc123",
|
|
want: "TOKEN=REDACTED",
|
|
},
|
|
{
|
|
name: "password",
|
|
in: "password=hunter2",
|
|
want: "password=REDACTED",
|
|
},
|
|
{
|
|
name: "secret",
|
|
in: "secret=topsecret",
|
|
want: "secret=REDACTED",
|
|
},
|
|
{
|
|
name: "api_key underscore",
|
|
in: "api_key=abc",
|
|
want: "api_key=REDACTED",
|
|
},
|
|
{
|
|
name: "api-key dash",
|
|
in: "api-key=abc",
|
|
want: "api-key=REDACTED",
|
|
},
|
|
{
|
|
name: "apikey contiguous",
|
|
in: "apikey=abc",
|
|
want: "apikey=REDACTED",
|
|
},
|
|
{
|
|
name: "preserves multiple non-sensitive keys",
|
|
in: "ref=TASK-5&token=abc&filter=open&limit=20",
|
|
want: "ref=TASK-5&token=REDACTED&filter=open&limit=20",
|
|
},
|
|
{
|
|
name: "lookalike key not redacted",
|
|
in: "tokenizer=on&ref=TASK-5",
|
|
want: "tokenizer=on&ref=TASK-5",
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := redactQueryString(tc.in)
|
|
if got != tc.want {
|
|
t.Fatalf("redactQueryString(%q) = %q, want %q", tc.in, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|