1
0
forked from baron/baron-sso

feat: add env-aware client log policy and const lint fixes

This commit is contained in:
Lectom C Han
2026-02-24 15:38:55 +09:00
parent 4ffe5110dd
commit aeb418fb9f
14 changed files with 701 additions and 47 deletions

View File

@@ -277,6 +277,8 @@ func main() {
// 3. Initialize Fiber
appEnv := getEnv("APP_ENV", "dev")
clientLogDebugFlag := getEnv("CLIENT_LOG_DEBUG", "")
clientDebugEnabled := logger.ClientDebugEnabled(appEnv, clientLogDebugFlag)
app := fiber.New(fiber.Config{
AppName: "Baron SSO Backend",
DisableStartupMessage: true, // Clean logs
@@ -367,6 +369,10 @@ func main() {
} else {
slog.Info("🔒 API Docs disabled in production")
}
slog.Info("Client log policy configured",
"app_env", appEnv,
"client_debug_enabled", clientDebugEnabled,
)
// Routes
app.Get("/", func(c *fiber.Ctx) error {
@@ -630,12 +636,20 @@ func main() {
if err := c.BodyParser(&req); err != nil {
return c.SendStatus(fiber.StatusBadRequest)
}
if !logger.ShouldAcceptClientLog(appEnv, clientLogDebugFlag, req.Level) {
return c.SendStatus(fiber.StatusOK)
}
level := logger.NormalizeClientLogLevel(req.Level)
if level == slog.LevelInfo && logger.ShouldFilterNoisyClientInfo(appEnv, clientLogDebugFlag, req.Message) {
return c.SendStatus(fiber.StatusOK)
}
// Prepare attributes for flattening
attrs := []any{
slog.String("source", "client"),
}
for k, v := range req.Data {
sanitizedData := logger.SanitizeClientLogData(req.Data)
for k, v := range sanitizedData {
// Skip svc if it's already set by the global logger to avoid confusion,
// or keep it as client_svc
if k == "svc" {
@@ -644,30 +658,7 @@ func main() {
attrs = append(attrs, slog.Any(k, v))
}
}
// Map and log with correct level
var level slog.Level
switch req.Level {
case "SEVERE", "ERROR":
level = slog.LevelError
case "WARNING", "WARN":
level = slog.LevelWarn
default:
level = slog.LevelInfo
}
// Filter out noisy client navigation logs
if level == slog.LevelInfo {
msg := strings.ToLower(req.Message)
if strings.Contains(msg, "navigating to") ||
strings.Contains(msg, "going to") ||
strings.Contains(msg, "redirecting to") ||
strings.Contains(msg, "full paths for routes") {
return c.SendStatus(fiber.StatusOK)
}
}
slog.Log(c.Context(), level, req.Message, attrs...)
slog.Log(c.Context(), level, logger.SanitizeClientLogMessage(req.Message), attrs...)
return c.SendStatus(fiber.StatusOK)
})

View File

@@ -0,0 +1,143 @@
package logger
import (
"log/slog"
"regexp"
"strings"
)
var sensitiveClientLogKeys = map[string]struct{}{
"password": {},
"currentpassword": {},
"newpassword": {},
"oldpassword": {},
"token": {},
"accesstoken": {},
"refreshtoken": {},
"secret": {},
"clientsecret": {},
"authorization": {},
"cookie": {},
"setcookie": {},
"verificationcode": {},
"code": {},
"loginchallenge": {},
"loginverifier": {},
"sessionjwt": {},
"accessjwt": {},
"refreshjwt": {},
}
var (
logJSONSensitivePattern = regexp.MustCompile(`(?i)"(password|currentpassword|newpassword|oldpassword|token|accesstoken|refreshtoken|secret|clientsecret|authorization|cookie|setcookie|verificationcode|code|loginchallenge|loginverifier|sessionjwt|accessjwt|refreshjwt)"\s*:\s*"[^"]*"`)
logKVPattern = regexp.MustCompile(`(?i)\b(password|current_password|currentpassword|new_password|newpassword|old_password|oldpassword|token|access_token|accesstoken|refresh_token|refreshtoken|authorization|cookie|session_jwt|sessionjwt|access_jwt|accessjwt|refresh_jwt|refreshjwt)\b\s*[:=]\s*([^\s,;]+)`)
)
func IsProductionEnv(appEnv string) bool {
env := strings.ToLower(strings.TrimSpace(appEnv))
return env == "prod" || env == "production"
}
func parseBoolFlag(raw string) bool {
switch strings.ToLower(strings.TrimSpace(raw)) {
case "1", "true", "yes", "y", "on":
return true
default:
return false
}
}
func ClientDebugEnabled(appEnv, productionDebugFlag string) bool {
if !IsProductionEnv(appEnv) {
return true
}
return parseBoolFlag(productionDebugFlag)
}
func NormalizeClientLogLevel(level string) slog.Level {
switch strings.ToUpper(strings.TrimSpace(level)) {
case "SEVERE", "ERROR":
return slog.LevelError
case "WARNING", "WARN":
return slog.LevelWarn
case "DEBUG", "FINE", "TRACE":
return slog.LevelDebug
default:
return slog.LevelInfo
}
}
func ShouldAcceptClientLog(appEnv, productionDebugFlag, level string) bool {
if ClientDebugEnabled(appEnv, productionDebugFlag) {
return true
}
return NormalizeClientLogLevel(level) >= slog.LevelWarn
}
func ShouldFilterNoisyClientInfo(appEnv, productionDebugFlag, message string) bool {
if ClientDebugEnabled(appEnv, productionDebugFlag) {
return false
}
msg := strings.ToLower(message)
return strings.Contains(msg, "navigating to") ||
strings.Contains(msg, "going to") ||
strings.Contains(msg, "redirecting to") ||
strings.Contains(msg, "full paths for routes")
}
func SanitizeClientLogMessage(message string) string {
if strings.TrimSpace(message) == "" {
return message
}
sanitized := logJSONSensitivePattern.ReplaceAllStringFunc(message, func(segment string) string {
parts := strings.SplitN(segment, ":", 2)
if len(parts) != 2 {
return segment
}
return parts[0] + `:"*****"`
})
sanitized = logKVPattern.ReplaceAllString(sanitized, `$1=*****`)
return sanitized
}
func SanitizeClientLogData(data map[string]interface{}) map[string]interface{} {
if len(data) == 0 {
return data
}
out := make(map[string]interface{}, len(data))
for k, v := range data {
if isSensitiveClientLogKey(k) {
out[k] = "*****"
continue
}
out[k] = sanitizeClientLogValue(v)
}
return out
}
func sanitizeClientLogValue(v interface{}) interface{} {
switch val := v.(type) {
case map[string]interface{}:
return SanitizeClientLogData(val)
case []interface{}:
next := make([]interface{}, len(val))
for i := range val {
next[i] = sanitizeClientLogValue(val[i])
}
return next
case string:
return SanitizeClientLogMessage(val)
default:
return val
}
}
func isSensitiveClientLogKey(key string) bool {
normalized := strings.ToLower(strings.TrimSpace(key))
normalized = strings.ReplaceAll(normalized, "-", "")
normalized = strings.ReplaceAll(normalized, "_", "")
normalized = strings.ReplaceAll(normalized, ".", "")
normalized = strings.ReplaceAll(normalized, " ", "")
_, ok := sensitiveClientLogKeys[normalized]
return ok
}

View File

@@ -0,0 +1,79 @@
package logger
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestClientDebugEnabled(t *testing.T) {
t.Run("non production enables debug by default", func(t *testing.T) {
assert.True(t, ClientDebugEnabled("dev", ""))
assert.True(t, ClientDebugEnabled("local", "false"))
})
t.Run("production disables debug by default", func(t *testing.T) {
assert.False(t, ClientDebugEnabled("production", ""))
assert.False(t, ClientDebugEnabled("prod", "false"))
})
t.Run("production accepts explicit debug override", func(t *testing.T) {
assert.True(t, ClientDebugEnabled("production", "true"))
assert.True(t, ClientDebugEnabled("production", "1"))
assert.True(t, ClientDebugEnabled("prod", "on"))
})
}
func TestShouldAcceptClientLog(t *testing.T) {
assert.False(t, ShouldAcceptClientLog("production", "", "INFO"))
assert.False(t, ShouldAcceptClientLog("production", "", "DEBUG"))
assert.True(t, ShouldAcceptClientLog("production", "", "WARN"))
assert.True(t, ShouldAcceptClientLog("production", "", "ERROR"))
assert.True(t, ShouldAcceptClientLog("production", "true", "INFO"))
assert.True(t, ShouldAcceptClientLog("dev", "", "INFO"))
}
func TestShouldFilterNoisyClientInfo(t *testing.T) {
assert.True(t, ShouldFilterNoisyClientInfo("production", "", "Navigating to /ko/signin"))
assert.False(t, ShouldFilterNoisyClientInfo("production", "true", "Navigating to /ko/signin"))
assert.False(t, ShouldFilterNoisyClientInfo("dev", "", "Navigating to /ko/signin"))
}
func TestSanitizeClientLogData(t *testing.T) {
input := map[string]interface{}{
"token": "raw-token",
"safe": "ok",
"nested": map[string]interface{}{
"new_password": "secret-1",
"path": "/ko/profile",
},
"arr": []interface{}{
map[string]interface{}{"authorization": "Bearer abc"},
"token=abc123",
},
}
result := SanitizeClientLogData(input)
assert.Equal(t, "*****", result["token"])
assert.Equal(t, "ok", result["safe"])
nested := result["nested"].(map[string]interface{})
assert.Equal(t, "*****", nested["new_password"])
assert.Equal(t, "/ko/profile", nested["path"])
arr := result["arr"].([]interface{})
first := arr[0].(map[string]interface{})
assert.Equal(t, "*****", first["authorization"])
assert.Equal(t, "token=*****", arr[1])
}
func TestSanitizeClientLogMessage(t *testing.T) {
msg := `FLUTTER_ERROR token=abc123 payload={"password":"hello","safe":"ok"} authorization:BearerX`
sanitized := SanitizeClientLogMessage(msg)
assert.NotContains(t, sanitized, "abc123")
assert.NotContains(t, sanitized, `"password":"hello"`)
assert.Contains(t, sanitized, `"password":"*****"`)
assert.Contains(t, sanitized, "token=*****")
assert.Contains(t, sanitized, "authorization=*****")
}