- Add internal/audit package with JSON() and JSONWithRoles() helpers
that use json.Marshal instead of fmt.Sprintf with %q
- Replace all fmt.Sprintf audit detail construction in:
- internal/server/server.go (10 occurrences)
- internal/ui/handlers_auth.go (4 occurrences)
- internal/grpcserver/auth.go (4 occurrences)
- Add tests for the helpers including edge-case Unicode,
null bytes, special characters, and odd argument counts
- Fix broken {"roles":%v} formatting that produced invalid JSON
Security: Audit log detail strings are now constructed via
json.Marshal, which correctly handles all Unicode edge cases
(U+2028, U+2029, null bytes, etc.) that fmt.Sprintf with %q
may mishandle. This prevents potential log injection or parsing
issues in audit event consumers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
164 lines
4.1 KiB
Go
164 lines
4.1 KiB
Go
package audit
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
)
|
|
|
|
func TestJSON(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
pairs []string
|
|
verify func(t *testing.T, result string)
|
|
}{
|
|
{
|
|
name: "single pair",
|
|
pairs: []string{"username", "alice"},
|
|
verify: func(t *testing.T, result string) {
|
|
var m map[string]string
|
|
if err := json.Unmarshal([]byte(result), &m); err != nil {
|
|
t.Fatalf("invalid JSON: %v", err)
|
|
}
|
|
if m["username"] != "alice" {
|
|
t.Fatalf("expected alice, got %s", m["username"])
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "multiple pairs",
|
|
pairs: []string{"jti", "abc-123", "reason", "logout"},
|
|
verify: func(t *testing.T, result string) {
|
|
var m map[string]string
|
|
if err := json.Unmarshal([]byte(result), &m); err != nil {
|
|
t.Fatalf("invalid JSON: %v", err)
|
|
}
|
|
if m["jti"] != "abc-123" {
|
|
t.Fatalf("expected abc-123, got %s", m["jti"])
|
|
}
|
|
if m["reason"] != "logout" {
|
|
t.Fatalf("expected logout, got %s", m["reason"])
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "special characters in values",
|
|
pairs: []string{"username", "user\"with\\quotes"},
|
|
verify: func(t *testing.T, result string) {
|
|
var m map[string]string
|
|
if err := json.Unmarshal([]byte(result), &m); err != nil {
|
|
t.Fatalf("invalid JSON for special chars: %v", err)
|
|
}
|
|
if m["username"] != "user\"with\\quotes" {
|
|
t.Fatalf("unexpected value: %s", m["username"])
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "unicode edge cases",
|
|
pairs: []string{"username", "user\u2028line\u2029sep"},
|
|
verify: func(t *testing.T, result string) {
|
|
var m map[string]string
|
|
if err := json.Unmarshal([]byte(result), &m); err != nil {
|
|
t.Fatalf("invalid JSON for unicode: %v", err)
|
|
}
|
|
if m["username"] != "user\u2028line\u2029sep" {
|
|
t.Fatalf("unexpected value: %s", m["username"])
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "null bytes in value",
|
|
pairs: []string{"data", "before\x00after"},
|
|
verify: func(t *testing.T, result string) {
|
|
var m map[string]string
|
|
if err := json.Unmarshal([]byte(result), &m); err != nil {
|
|
t.Fatalf("invalid JSON for null bytes: %v", err)
|
|
}
|
|
if m["data"] != "before\x00after" {
|
|
t.Fatalf("unexpected value: %q", m["data"])
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "odd number of args returns empty object",
|
|
pairs: []string{"key"},
|
|
verify: func(t *testing.T, result string) {
|
|
if result != "{}" {
|
|
t.Fatalf("expected {}, got %s", result)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "no args returns empty object",
|
|
pairs: nil,
|
|
verify: func(t *testing.T, result string) {
|
|
if result != "{}" {
|
|
t.Fatalf("expected {}, got %s", result)
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
result := JSON(tc.pairs...)
|
|
tc.verify(t, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestJSONWithRoles(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
roles []string
|
|
verify func(t *testing.T, result string)
|
|
}{
|
|
{
|
|
name: "multiple roles",
|
|
roles: []string{"admin", "editor"},
|
|
verify: func(t *testing.T, result string) {
|
|
var m map[string][]string
|
|
if err := json.Unmarshal([]byte(result), &m); err != nil {
|
|
t.Fatalf("invalid JSON: %v", err)
|
|
}
|
|
if len(m["roles"]) != 2 || m["roles"][0] != "admin" || m["roles"][1] != "editor" {
|
|
t.Fatalf("unexpected roles: %v", m["roles"])
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "empty roles",
|
|
roles: []string{},
|
|
verify: func(t *testing.T, result string) {
|
|
var m map[string][]string
|
|
if err := json.Unmarshal([]byte(result), &m); err != nil {
|
|
t.Fatalf("invalid JSON: %v", err)
|
|
}
|
|
if len(m["roles"]) != 0 {
|
|
t.Fatalf("expected empty roles, got %v", m["roles"])
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "roles with special characters",
|
|
roles: []string{"role\"special"},
|
|
verify: func(t *testing.T, result string) {
|
|
var m map[string][]string
|
|
if err := json.Unmarshal([]byte(result), &m); err != nil {
|
|
t.Fatalf("invalid JSON: %v", err)
|
|
}
|
|
if m["roles"][0] != "role\"special" {
|
|
t.Fatalf("unexpected role: %s", m["roles"][0])
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
result := JSONWithRoles(tc.roles)
|
|
tc.verify(t, result)
|
|
})
|
|
}
|
|
}
|