Merge branch 'main' into feat/ldap-groups

This commit is contained in:
Stavros
2026-01-15 16:01:41 +02:00
32 changed files with 522 additions and 246 deletions

View File

@@ -2,8 +2,6 @@
# The base URL where Tinyauth is accessible
TINYAUTH_APPURL="https://auth.example.com"
# Log level: trace, debug, info, warn, error
TINYAUTH_LOGLEVEL="info"
# Directory for static resources
TINYAUTH_RESOURCESDIR="/data/resources"
# Path to SQLite database file
@@ -14,8 +12,21 @@ TINYAUTH_DISABLEANALYTICS="false"
TINYAUTH_DISABLERESOURCES="false"
# Disable UI warning messages
TINYAUTH_DISABLEUIWARNINGS="false"
# Logging Configuration
# Log level: trace, debug, info, warn, error
TINYAUTH_LOG_LEVEL="info"
# Enable JSON formatted logs
TINYAUTH_LOGJSON="false"
TINYAUTH_LOG_JSON="false"
# Specific Log stream configurations
# APP and HTTP log streams are enabled by default, and use the global log level unless overridden
TINYAUTH_LOG_STREAMS_APP_ENABLED="true"
TINYAUTH_LOG_STREAMS_APP_LEVEL="info"
TINYAUTH_LOG_STREAMS_HTTP_ENABLED="true"
TINYAUTH_LOG_STREAMS_HTTP_LEVEL="info"
TINYAUTH_LOG_STREAMS_AUDIT_ENABLED="false"
TINYAUTH_LOG_STREAMS_AUDIT_LEVEL="info"
# Server Configuration

View File

@@ -3,13 +3,10 @@ package main
import (
"errors"
"fmt"
"os"
"strings"
"time"
"github.com/charmbracelet/huh"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
"github.com/traefik/paerser/cli"
"golang.org/x/crypto/bcrypt"
)
@@ -43,7 +40,7 @@ func createUserCmd() *cli.Command {
Configuration: tCfg,
Resources: loaders,
Run: func(_ []string) error {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Caller().Logger().Level(zerolog.InfoLevel)
tlog.NewSimpleLogger().Init()
if tCfg.Interactive {
form := huh.NewForm(
@@ -77,7 +74,7 @@ func createUserCmd() *cli.Command {
return errors.New("username and password cannot be empty")
}
log.Info().Str("username", tCfg.Username).Msg("Creating user")
tlog.App.Info().Str("username", tCfg.Username).Msg("Creating user")
passwd, err := bcrypt.GenerateFromPassword([]byte(tCfg.Password), bcrypt.DefaultCost)
if err != nil {
@@ -90,7 +87,7 @@ func createUserCmd() *cli.Command {
passwdStr = strings.ReplaceAll(passwdStr, "$", "$$")
}
log.Info().Str("user", fmt.Sprintf("%s:%s", tCfg.Username, passwdStr)).Msg("User created")
tlog.App.Info().Str("user", fmt.Sprintf("%s:%s", tCfg.Username, passwdStr)).Msg("User created")
return nil
},

View File

@@ -5,15 +5,13 @@ import (
"fmt"
"os"
"strings"
"time"
"github.com/steveiliop56/tinyauth/internal/utils"
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
"github.com/charmbracelet/huh"
"github.com/mdp/qrterminal/v3"
"github.com/pquerna/otp/totp"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/traefik/paerser/cli"
)
@@ -42,7 +40,7 @@ func generateTotpCmd() *cli.Command {
Configuration: tCfg,
Resources: loaders,
Run: func(_ []string) error {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Caller().Logger().Level(zerolog.InfoLevel)
tlog.NewSimpleLogger().Init()
if tCfg.Interactive {
form := huh.NewForm(
@@ -91,9 +89,9 @@ func generateTotpCmd() *cli.Command {
secret := key.Secret()
log.Info().Str("secret", secret).Msg("Generated TOTP secret")
tlog.App.Info().Str("secret", secret).Msg("Generated TOTP secret")
log.Info().Msg("Generated QR code")
tlog.App.Info().Msg("Generated QR code")
config := qrterminal.Config{
Level: qrterminal.L,
@@ -112,7 +110,7 @@ func generateTotpCmd() *cli.Command {
user.Password = strings.ReplaceAll(user.Password, "$", "$$")
}
log.Info().Str("user", fmt.Sprintf("%s:%s:%s", user.Username, user.Password, user.TotpSecret)).Msg("Add the totp secret to your authenticator app then use the verify command to ensure everything is working correctly.")
tlog.App.Info().Str("user", fmt.Sprintf("%s:%s:%s", user.Username, user.Password, user.TotpSecret)).Msg("Add the totp secret to your authenticator app then use the verify command to ensure everything is working correctly.")
return nil
},

View File

@@ -9,8 +9,7 @@ import (
"os"
"time"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
"github.com/traefik/paerser/cli"
)
@@ -27,7 +26,7 @@ func healthcheckCmd() *cli.Command {
Resources: nil,
AllowArg: true,
Run: func(args []string) error {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Caller().Logger().Level(zerolog.InfoLevel)
tlog.NewSimpleLogger().Init()
appUrl := os.Getenv("TINYAUTH_APPURL")
@@ -39,7 +38,7 @@ func healthcheckCmd() *cli.Command {
return errors.New("TINYAUTH_APPURL is not set and no argument was provided")
}
log.Info().Str("app_url", appUrl).Msg("Performing health check")
tlog.App.Info().Str("app_url", appUrl).Msg("Performing health check")
client := http.Client{
Timeout: 30 * time.Second,
@@ -77,7 +76,7 @@ func healthcheckCmd() *cli.Command {
return fmt.Errorf("failed to decode response: %w", err)
}
log.Info().Interface("response", healthResp).Msg("Tinyauth is healthy")
tlog.App.Info().Interface("response", healthResp).Msg("Tinyauth is healthy")
return nil
},

View File

@@ -2,22 +2,18 @@ package main
import (
"fmt"
"os"
"strings"
"time"
"github.com/steveiliop56/tinyauth/internal/bootstrap"
"github.com/steveiliop56/tinyauth/internal/config"
"github.com/steveiliop56/tinyauth/internal/utils/loaders"
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/traefik/paerser/cli"
)
func NewTinyauthCmdConfiguration() *config.Config {
return &config.Config{
LogLevel: "info",
ResourcesDir: "./resources",
DatabasePath: "./tinyauth.db",
Server: config.ServerConfig{
@@ -39,6 +35,24 @@ func NewTinyauthCmdConfiguration() *config.Config {
Insecure: false,
SearchFilter: "(uid=%s)",
},
Log: config.LogConfig{
Level: "info",
Json: false,
Streams: config.LogStreams{
HTTP: config.LogStreamConfig{
Enabled: true,
Level: "",
},
App: config.LogStreamConfig{
Enabled: true,
Level: "",
},
Audit: config.LogStreamConfig{
Enabled: false,
Level: "",
},
},
},
Experimental: config.ExperimentalConfig{
ConfigFile: "",
},
@@ -102,25 +116,14 @@ func main() {
}
func runCmd(cfg config.Config) error {
logLevel, err := zerolog.ParseLevel(strings.ToLower(cfg.LogLevel))
logger := tlog.NewLogger(cfg.Log)
logger.Init()
if err != nil {
log.Error().Err(err).Msg("Invalid or missing log level, defaulting to info")
} else {
zerolog.SetGlobalLevel(logLevel)
}
log.Logger = log.With().Caller().Logger()
if !cfg.LogJSON {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339})
}
log.Info().Str("version", config.Version).Msg("Starting tinyauth")
tlog.App.Info().Str("version", config.Version).Msg("Starting tinyauth")
app := bootstrap.NewBootstrapApp(cfg)
err = app.Setup()
err := app.Setup()
if err != nil {
return fmt.Errorf("failed to bootstrap app: %w", err)

View File

@@ -3,15 +3,12 @@ package main
import (
"errors"
"fmt"
"os"
"time"
"github.com/steveiliop56/tinyauth/internal/utils"
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
"github.com/charmbracelet/huh"
"github.com/pquerna/otp/totp"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/traefik/paerser/cli"
"golang.org/x/crypto/bcrypt"
)
@@ -47,7 +44,7 @@ func verifyUserCmd() *cli.Command {
Configuration: tCfg,
Resources: loaders,
Run: func(_ []string) error {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Caller().Logger().Level(zerolog.InfoLevel)
tlog.NewSimpleLogger().Init()
if tCfg.Interactive {
form := huh.NewForm(
@@ -101,9 +98,9 @@ func verifyUserCmd() *cli.Command {
if user.TotpSecret == "" {
if tCfg.Totp != "" {
log.Warn().Msg("User does not have TOTP secret")
tlog.App.Warn().Msg("User does not have TOTP secret")
}
log.Info().Msg("User verified")
tlog.App.Info().Msg("User verified")
return nil
}
@@ -113,7 +110,7 @@ func verifyUserCmd() *cli.Command {
return fmt.Errorf("TOTP code incorrect")
}
log.Info().Msg("User verified")
tlog.App.Info().Msg("User verified")
return nil
},

View File

@@ -2,8 +2,6 @@
# The base URL where Tinyauth is accessible
appUrl: "https://auth.example.com"
# Log level: trace, debug, info, warn, error
logLevel: "info"
# Directory for static resources
resourcesDir: "./resources"
# Path to SQLite database file
@@ -14,8 +12,22 @@ disableAnalytics: false
disableResources: false
# Disable UI warning messages
disableUIWarnings: false
# Enable JSON formatted logs
logJSON: false
# Logging Configuration
log:
# Log level: trace, debug, info, warn, error
level: "info"
json: false
streams:
app:
enabled: true
level: "warn"
http:
enabled: true
level: "debug"
audit:
enabled: false
level: "info"
# Server Configuration
server:

View File

@@ -12,7 +12,7 @@
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@tailwindcss/vite": "^4.1.18",
"@tanstack/react-query": "^5.90.16",
"@tanstack/react-query": "^5.90.17",
"axios": "^1.13.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -24,8 +24,8 @@
"next-themes": "^0.4.6",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-hook-form": "^7.71.0",
"react-i18next": "^16.5.2",
"react-hook-form": "^7.71.1",
"react-i18next": "^16.5.3",
"react-markdown": "^10.1.0",
"react-router": "^7.12.0",
"sonner": "^2.0.7",
@@ -36,7 +36,7 @@
"devDependencies": {
"@eslint/js": "^9.39.2",
"@tanstack/eslint-plugin-query": "^5.91.2",
"@types/node": "^25.0.7",
"@types/node": "^25.0.8",
"@types/react": "^19.2.8",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.2",
@@ -44,7 +44,7 @@
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.26",
"globals": "^17.0.0",
"prettier": "3.7.4",
"prettier": "3.8.0",
"tw-animate-css": "^1.4.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.53.0",
@@ -339,9 +339,9 @@
"@tanstack/eslint-plugin-query": ["@tanstack/eslint-plugin-query@5.91.2", "", { "dependencies": { "@typescript-eslint/utils": "^8.44.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" } }, "sha512-UPeWKl/Acu1IuuHJlsN+eITUHqAaa9/04geHHPedY8siVarSaWprY0SVMKrkpKfk5ehRT7+/MZ5QwWuEtkWrFw=="],
"@tanstack/query-core": ["@tanstack/query-core@5.90.16", "", {}, "sha512-MvtWckSVufs/ja463/K4PyJeqT+HMlJWtw6PrCpywznd2NSgO3m4KwO9RqbFqGg6iDE8vVMFWMeQI4Io3eEYww=="],
"@tanstack/query-core": ["@tanstack/query-core@5.90.17", "", {}, "sha512-hDww+RyyYhjhUfoYQ4es6pbgxY7LNiPWxt4l1nJqhByjndxJ7HIjDxTBtfvMr5HwjYavMrd+ids5g4Rfev3lVQ=="],
"@tanstack/react-query": ["@tanstack/react-query@5.90.16", "", { "dependencies": { "@tanstack/query-core": "5.90.16" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-bpMGOmV4OPmif7TNMteU/Ehf/hoC0Kf98PDc0F4BZkFrEapRMEqI/V6YS0lyzwSV6PQpY1y4xxArUIfBW5LVxQ=="],
"@tanstack/react-query": ["@tanstack/react-query@5.90.17", "", { "dependencies": { "@tanstack/query-core": "5.90.17" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-PGc2u9KLwohDUSchjW9MZqeDQJfJDON7y4W7REdNBgiFKxQy+Pf7eGjiFWEj5xPqKzAeHYdAb62IWI1a9UJyGQ=="],
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
@@ -365,7 +365,7 @@
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
"@types/node": ["@types/node@25.0.7", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-C/er7DlIZgRJO7WtTdYovjIFzGsz0I95UlMyR9anTb4aCpBSRWe5Jc1/RvLKUfzmOxHPGjSE5+63HgLtndxU4w=="],
"@types/node": ["@types/node@25.0.8", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg=="],
"@types/react": ["@types/react@19.2.8", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg=="],
@@ -781,7 +781,7 @@
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="],
"prettier": ["prettier@3.8.0", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA=="],
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
@@ -795,9 +795,9 @@
"react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="],
"react-hook-form": ["react-hook-form@7.71.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-oFDt/iIFMV9ZfV52waONXzg4xuSlbwKUPvXVH2jumL1me5qFhBMc4knZxuXiZ2+j6h546sYe3ZKJcg/900/iHw=="],
"react-hook-form": ["react-hook-form@7.71.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w=="],
"react-i18next": ["react-i18next@16.5.2", "", { "dependencies": { "@babel/runtime": "^7.28.4", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 25.6.2", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-GG/SBVxx9dvrO1uCs8VYdKfOP8NEBUhNP+2VDQLCifRJ8DL1qPq296k2ACNGyZMDe7iyIlz/LMJTQOs8HXSRvw=="],
"react-i18next": ["react-i18next@16.5.3", "", { "dependencies": { "@babel/runtime": "^7.28.4", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 25.6.2", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-fo+/NNch37zqxOzlBYrWMx0uy/yInPkRfjSuy4lqKdaecR17nvCHnEUt3QyzA8XjQ2B/0iW/5BhaHR3ZmukpGw=="],
"react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="],

View File

@@ -18,7 +18,7 @@
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@tailwindcss/vite": "^4.1.18",
"@tanstack/react-query": "^5.90.16",
"@tanstack/react-query": "^5.90.17",
"axios": "^1.13.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -30,8 +30,8 @@
"next-themes": "^0.4.6",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-hook-form": "^7.71.0",
"react-i18next": "^16.5.2",
"react-hook-form": "^7.71.1",
"react-i18next": "^16.5.3",
"react-markdown": "^10.1.0",
"react-router": "^7.12.0",
"sonner": "^2.0.7",
@@ -42,7 +42,7 @@
"devDependencies": {
"@eslint/js": "^9.39.2",
"@tanstack/eslint-plugin-query": "^5.91.2",
"@types/node": "^25.0.7",
"@types/node": "^25.0.8",
"@types/react": "^19.2.8",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.2",
@@ -50,7 +50,7 @@
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.26",
"globals": "^17.0.0",
"prettier": "3.7.4",
"prettier": "3.8.0",
"tw-animate-css": "^1.4.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.53.0",

8
go.mod
View File

@@ -21,10 +21,10 @@ require (
github.com/traefik/paerser v0.2.2
github.com/weppos/publicsuffix-go v0.50.2
golang.org/x/crypto v0.47.0
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546
golang.org/x/oauth2 v0.34.0
gotest.tools/v3 v3.5.2
modernc.org/sqlite v1.43.0
modernc.org/sqlite v1.44.0
)
require (
@@ -91,7 +91,7 @@ require (
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
@@ -119,7 +119,7 @@ require (
golang.org/x/text v0.33.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.66.10 // indirect
modernc.org/libc v1.67.4 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
rsc.io/qr v0.2.0 // indirect

28
go.sum
View File

@@ -140,6 +140,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3Ar
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
@@ -215,8 +217,8 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
@@ -301,8 +303,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
@@ -369,18 +371,20 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
modernc.org/libc v1.67.4 h1:zZGmCMUVPORtKv95c2ReQN5VDjvkoRm9GWPTEPuvlWg=
modernc.org/libc v1.67.4/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
@@ -389,8 +393,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.43.0 h1:8YqiFx3G1VhHTXO2Q00bl1Wz9KhS9Q5okwfp9Y97VnA=
modernc.org/sqlite v1.43.0/go.mod h1:+VkC6v3pLOAE0A0uVucQEcbVW0I5nHCeDaBf+DpsQT8=
modernc.org/sqlite v1.44.0 h1:YjCKJnzZde2mLVy0cMKTSL4PxCmbIguOq9lGp8ZvGOc=
modernc.org/sqlite v1.44.0/go.mod h1:2Dq41ir5/qri7QJJJKNZcP4UF7TsX/KNeykYgPDtGhE=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=

View File

@@ -16,8 +16,7 @@ import (
"github.com/steveiliop56/tinyauth/internal/controller"
"github.com/steveiliop56/tinyauth/internal/repository"
"github.com/steveiliop56/tinyauth/internal/utils"
"github.com/rs/zerolog/log"
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
)
type BootstrapApp struct {
@@ -103,13 +102,13 @@ func (app *BootstrapApp) Setup() error {
app.context.redirectCookieName = fmt.Sprintf("%s-%s", config.RedirectCookieName, cookieId)
// Dumps
log.Trace().Interface("config", app.config).Msg("Config dump")
log.Trace().Interface("users", app.context.users).Msg("Users dump")
log.Trace().Interface("oauthProviders", app.context.oauthProviders).Msg("OAuth providers dump")
log.Trace().Str("cookieDomain", app.context.cookieDomain).Msg("Cookie domain")
log.Trace().Str("sessionCookieName", app.context.sessionCookieName).Msg("Session cookie name")
log.Trace().Str("csrfCookieName", app.context.csrfCookieName).Msg("CSRF cookie name")
log.Trace().Str("redirectCookieName", app.context.redirectCookieName).Msg("Redirect cookie name")
tlog.App.Trace().Interface("config", app.config).Msg("Config dump")
tlog.App.Trace().Interface("users", app.context.users).Msg("Users dump")
tlog.App.Trace().Interface("oauthProviders", app.context.oauthProviders).Msg("OAuth providers dump")
tlog.App.Trace().Str("cookieDomain", app.context.cookieDomain).Msg("Cookie domain")
tlog.App.Trace().Str("sessionCookieName", app.context.sessionCookieName).Msg("Session cookie name")
tlog.App.Trace().Str("csrfCookieName", app.context.csrfCookieName).Msg("CSRF cookie name")
tlog.App.Trace().Str("redirectCookieName", app.context.redirectCookieName).Msg("Redirect cookie name")
// Database
db, err := app.SetupDatabase(app.config.DatabasePath)
@@ -153,7 +152,7 @@ func (app *BootstrapApp) Setup() error {
})
}
log.Debug().Interface("providers", configuredProviders).Msg("Authentication providers")
tlog.App.Debug().Interface("providers", configuredProviders).Msg("Authentication providers")
if len(configuredProviders) == 0 {
return fmt.Errorf("no authentication providers configured")
@@ -169,28 +168,28 @@ func (app *BootstrapApp) Setup() error {
}
// Start db cleanup routine
log.Debug().Msg("Starting database cleanup routine")
tlog.App.Debug().Msg("Starting database cleanup routine")
go app.dbCleanup(queries)
// If analytics are not disabled, start heartbeat
if !app.config.DisableAnalytics {
log.Debug().Msg("Starting heartbeat routine")
tlog.App.Debug().Msg("Starting heartbeat routine")
go app.heartbeat()
}
// If we have an socket path, bind to it
if app.config.Server.SocketPath != "" {
if _, err := os.Stat(app.config.Server.SocketPath); err == nil {
log.Info().Msgf("Removing existing socket file %s", app.config.Server.SocketPath)
tlog.App.Info().Msgf("Removing existing socket file %s", app.config.Server.SocketPath)
err := os.Remove(app.config.Server.SocketPath)
if err != nil {
return fmt.Errorf("failed to remove existing socket file: %w", err)
}
}
log.Info().Msgf("Starting server on unix socket %s", app.config.Server.SocketPath)
tlog.App.Info().Msgf("Starting server on unix socket %s", app.config.Server.SocketPath)
if err := router.RunUnix(app.config.Server.SocketPath); err != nil {
log.Fatal().Err(err).Msg("Failed to start server")
tlog.App.Fatal().Err(err).Msg("Failed to start server")
}
return nil
@@ -198,9 +197,9 @@ func (app *BootstrapApp) Setup() error {
// Start server
address := fmt.Sprintf("%s:%d", app.config.Server.Address, app.config.Server.Port)
log.Info().Msgf("Starting server on %s", address)
tlog.App.Info().Msgf("Starting server on %s", address)
if err := router.Run(address); err != nil {
log.Fatal().Err(err).Msg("Failed to start server")
tlog.App.Fatal().Err(err).Msg("Failed to start server")
}
return nil
@@ -223,7 +222,7 @@ func (app *BootstrapApp) heartbeat() {
bodyJson, err := json.Marshal(body)
if err != nil {
log.Error().Err(err).Msg("Failed to marshal heartbeat body")
tlog.App.Error().Err(err).Msg("Failed to marshal heartbeat body")
return
}
@@ -234,12 +233,12 @@ func (app *BootstrapApp) heartbeat() {
heartbeatURL := config.ApiServer + "/v1/instances/heartbeat"
for ; true; <-ticker.C {
log.Debug().Msg("Sending heartbeat")
tlog.App.Debug().Msg("Sending heartbeat")
req, err := http.NewRequest(http.MethodPost, heartbeatURL, bytes.NewReader(bodyJson))
if err != nil {
log.Error().Err(err).Msg("Failed to create heartbeat request")
tlog.App.Error().Err(err).Msg("Failed to create heartbeat request")
continue
}
@@ -248,14 +247,14 @@ func (app *BootstrapApp) heartbeat() {
res, err := client.Do(req)
if err != nil {
log.Error().Err(err).Msg("Failed to send heartbeat")
tlog.App.Error().Err(err).Msg("Failed to send heartbeat")
continue
}
res.Body.Close()
if res.StatusCode != 200 && res.StatusCode != 201 {
log.Debug().Str("status", res.Status).Msg("Heartbeat returned non-200/201 status")
tlog.App.Debug().Str("status", res.Status).Msg("Heartbeat returned non-200/201 status")
}
}
}
@@ -266,10 +265,10 @@ func (app *BootstrapApp) dbCleanup(queries *repository.Queries) {
ctx := context.Background()
for ; true; <-ticker.C {
log.Debug().Msg("Cleaning up old database sessions")
tlog.App.Debug().Msg("Cleaning up old database sessions")
err := queries.DeleteExpiredSessions(ctx, time.Now().Unix())
if err != nil {
log.Error().Err(err).Msg("Failed to clean up old database sessions")
tlog.App.Error().Err(err).Msg("Failed to clean up old database sessions")
}
}
}

View File

@@ -3,8 +3,7 @@ package bootstrap
import (
"github.com/steveiliop56/tinyauth/internal/repository"
"github.com/steveiliop56/tinyauth/internal/service"
"github.com/rs/zerolog/log"
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
)
type Services struct {
@@ -34,7 +33,7 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er
if err == nil {
services.ldapService = ldapService
} else {
log.Warn().Err(err).Msg("Failed to initialize LDAP service, continuing without it")
tlog.App.Warn().Err(err).Msg("Failed to initialize LDAP service, continuing without it")
}
dockerService := service.NewDockerService()

View File

@@ -16,13 +16,11 @@ var RedirectCookieName = "tinyauth-redirect"
type Config struct {
AppURL string `description:"The base URL where the app is hosted." yaml:"appUrl"`
LogLevel string `description:"Log level (trace, debug, info, warn, error)." yaml:"logLevel"`
ResourcesDir string `description:"The directory where resources are stored." yaml:"resourcesDir"`
DatabasePath string `description:"The path to the database file." yaml:"databasePath"`
DisableAnalytics bool `description:"Disable analytics." yaml:"disableAnalytics"`
DisableResources bool `description:"Disable resources server." yaml:"disableResources"`
DisableUIWarnings bool `description:"Disable UI warnings." yaml:"disableUIWarnings"`
LogJSON bool `description:"Enable JSON formatted logs." yaml:"logJSON"`
Server ServerConfig `description:"Server configuration." yaml:"server"`
Auth AuthConfig `description:"Authentication configuration." yaml:"auth"`
Apps map[string]App `description:"Application ACLs configuration." yaml:"apps"`
@@ -30,6 +28,7 @@ type Config struct {
UI UIConfig `description:"UI customization." yaml:"ui"`
Ldap LdapConfig `description:"LDAP configuration." yaml:"ldap"`
Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental"`
Log LogConfig `description:"Logging configuration." yaml:"log"`
}
type ServerConfig struct {
@@ -78,6 +77,23 @@ type LdapConfig struct {
AuthKey string `description:"Certificate key for mTLS authentication." yaml:"authKey"`
}
type LogConfig struct {
Level string `description:"Log level (trace, debug, info, warn, error)." yaml:"level"`
Json bool `description:"Enable JSON formatted logs." yaml:"json"`
Streams LogStreams `description:"Configuration for specific log streams." yaml:"streams"`
}
type LogStreams struct {
HTTP LogStreamConfig `description:"HTTP request logging." yaml:"http"`
App LogStreamConfig `description:"Application logging." yaml:"app"`
Audit LogStreamConfig `description:"Audit logging." yaml:"audit"`
}
type LogStreamConfig struct {
Enabled bool `description:"Enable this log stream." yaml:"enabled"`
Level string `description:"Log level for this stream. Use global if empty." yaml:"level"`
}
type ExperimentalConfig struct {
ConfigFile string `description:"Path to config file." yaml:"-"`
}

View File

@@ -5,9 +5,9 @@ import (
"net/url"
"github.com/steveiliop56/tinyauth/internal/utils"
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
)
type UserContextResponse struct {
@@ -61,7 +61,7 @@ type ContextController struct {
func NewContextController(config ContextControllerConfig, router *gin.RouterGroup) *ContextController {
if config.DisableUIWarnings {
log.Warn().Msg("UI warnings are disabled. This may expose users to security risks. Proceed with caution.")
tlog.App.Warn().Msg("UI warnings are disabled. This may expose users to security risks. Proceed with caution.")
}
return &ContextController{
@@ -94,7 +94,7 @@ func (controller *ContextController) userContextHandler(c *gin.Context) {
}
if err != nil {
log.Debug().Err(err).Msg("No user context found in request")
tlog.App.Debug().Err(err).Msg("No user context found in request")
userContext.Status = 401
userContext.Message = "Unauthorized"
userContext.IsLoggedIn = false
@@ -108,7 +108,7 @@ func (controller *ContextController) userContextHandler(c *gin.Context) {
func (controller *ContextController) appContextHandler(c *gin.Context) {
appUrl, err := url.Parse(controller.config.AppURL)
if err != nil {
log.Error().Err(err).Msg("Failed to parse app URL")
tlog.App.Error().Err(err).Msg("Failed to parse app URL")
c.JSON(500, gin.H{
"status": 500,
"message": "Internal Server Error",

View File

@@ -7,6 +7,7 @@ import (
"github.com/steveiliop56/tinyauth/internal/config"
"github.com/steveiliop56/tinyauth/internal/controller"
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
"github.com/gin-gonic/gin"
"gotest.tools/v3/assert"
@@ -48,6 +49,8 @@ var userContext = config.UserContext{
}
func setupContextController(middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder) {
tlog.NewSimpleLogger().Init()
// Setup
gin.SetMode(gin.TestMode)
router := gin.Default()

View File

@@ -10,10 +10,10 @@ import (
"github.com/steveiliop56/tinyauth/internal/repository"
"github.com/steveiliop56/tinyauth/internal/service"
"github.com/steveiliop56/tinyauth/internal/utils"
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
"github.com/gin-gonic/gin"
"github.com/google/go-querystring/query"
"github.com/rs/zerolog/log"
)
type OAuthRequest struct {
@@ -55,7 +55,7 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
err := c.BindUri(&req)
if err != nil {
log.Error().Err(err).Msg("Failed to bind URI")
tlog.App.Error().Err(err).Msg("Failed to bind URI")
c.JSON(400, gin.H{
"status": 400,
"message": "Bad Request",
@@ -66,7 +66,7 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
service, exists := controller.broker.GetService(req.Provider)
if !exists {
log.Warn().Msgf("OAuth provider not found: %s", req.Provider)
tlog.App.Warn().Msgf("OAuth provider not found: %s", req.Provider)
c.JSON(404, gin.H{
"status": 404,
"message": "Not Found",
@@ -83,12 +83,12 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
isRedirectSafe := utils.IsRedirectSafe(redirectURI, controller.config.CookieDomain)
if !isRedirectSafe {
log.Warn().Str("redirect_uri", redirectURI).Msg("Unsafe redirect URI detected, ignoring")
tlog.App.Warn().Str("redirect_uri", redirectURI).Msg("Unsafe redirect URI detected, ignoring")
redirectURI = ""
}
if redirectURI != "" && isRedirectSafe {
log.Debug().Msg("Setting redirect URI cookie")
tlog.App.Debug().Msg("Setting redirect URI cookie")
c.SetCookie(controller.config.RedirectCookieName, redirectURI, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
}
@@ -104,7 +104,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
err := c.BindUri(&req)
if err != nil {
log.Error().Err(err).Msg("Failed to bind URI")
tlog.App.Error().Err(err).Msg("Failed to bind URI")
c.JSON(400, gin.H{
"status": 400,
"message": "Bad Request",
@@ -116,7 +116,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
csrfCookie, err := c.Cookie(controller.config.CSRFCookieName)
if err != nil || state != csrfCookie {
log.Warn().Err(err).Msg("CSRF token mismatch or cookie missing")
tlog.App.Warn().Err(err).Msg("CSRF token mismatch or cookie missing")
c.SetCookie(controller.config.CSRFCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
return
@@ -128,14 +128,14 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
service, exists := controller.broker.GetService(req.Provider)
if !exists {
log.Warn().Msgf("OAuth provider not found: %s", req.Provider)
tlog.App.Warn().Msgf("OAuth provider not found: %s", req.Provider)
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
return
}
err = service.VerifyCode(code)
if err != nil {
log.Error().Err(err).Msg("Failed to verify OAuth code")
tlog.App.Error().Err(err).Msg("Failed to verify OAuth code")
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
return
}
@@ -143,26 +143,27 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
user, err := controller.broker.GetUser(req.Provider)
if err != nil {
log.Error().Err(err).Msg("Failed to get user from OAuth provider")
tlog.App.Error().Err(err).Msg("Failed to get user from OAuth provider")
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
return
}
if user.Email == "" {
log.Error().Msg("OAuth provider did not return an email")
tlog.App.Error().Msg("OAuth provider did not return an email")
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
return
}
if !controller.auth.IsEmailWhitelisted(user.Email) {
log.Warn().Str("email", user.Email).Msg("Email not whitelisted")
tlog.App.Warn().Str("email", user.Email).Msg("Email not whitelisted")
tlog.AuditLoginFailure(c, user.Email, req.Provider, "email not whitelisted")
queries, err := query.Values(config.UnauthorizedQuery{
Username: user.Email,
})
if err != nil {
log.Error().Err(err).Msg("Failed to encode unauthorized query")
tlog.App.Error().Err(err).Msg("Failed to encode unauthorized query")
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
return
}
@@ -174,20 +175,20 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
var name string
if strings.TrimSpace(user.Name) != "" {
log.Debug().Msg("Using name from OAuth provider")
tlog.App.Debug().Msg("Using name from OAuth provider")
name = user.Name
} else {
log.Debug().Msg("No name from OAuth provider, using pseudo name")
tlog.App.Debug().Msg("No name from OAuth provider, using pseudo name")
name = fmt.Sprintf("%s (%s)", utils.Capitalize(strings.Split(user.Email, "@")[0]), strings.Split(user.Email, "@")[1])
}
var username string
if strings.TrimSpace(user.PreferredUsername) != "" {
log.Debug().Msg("Using preferred username from OAuth provider")
tlog.App.Debug().Msg("Using preferred username from OAuth provider")
username = user.PreferredUsername
} else {
log.Debug().Msg("No preferred username from OAuth provider, using pseudo username")
tlog.App.Debug().Msg("No preferred username from OAuth provider, using pseudo username")
username = strings.Replace(user.Email, "@", "_", -1)
}
@@ -201,20 +202,22 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
OAuthSub: user.Sub,
}
log.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
tlog.App.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
err = controller.auth.CreateSessionCookie(c, &sessionCookie)
if err != nil {
log.Error().Err(err).Msg("Failed to create session cookie")
tlog.App.Error().Err(err).Msg("Failed to create session cookie")
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
return
}
tlog.AuditLoginSuccess(c, sessionCookie.Username, sessionCookie.Provider)
redirectURI, err := c.Cookie(controller.config.RedirectCookieName)
if err != nil || !utils.IsRedirectSafe(redirectURI, controller.config.CookieDomain) {
log.Debug().Msg("No redirect URI cookie found, redirecting to app root")
tlog.App.Debug().Msg("No redirect URI cookie found, redirecting to app root")
c.Redirect(http.StatusTemporaryRedirect, controller.config.AppURL)
return
}
@@ -224,7 +227,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
})
if err != nil {
log.Error().Err(err).Msg("Failed to encode redirect URI query")
tlog.App.Error().Err(err).Msg("Failed to encode redirect URI query")
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
return
}

View File

@@ -9,10 +9,10 @@ import (
"github.com/steveiliop56/tinyauth/internal/config"
"github.com/steveiliop56/tinyauth/internal/service"
"github.com/steveiliop56/tinyauth/internal/utils"
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
"github.com/gin-gonic/gin"
"github.com/google/go-querystring/query"
"github.com/rs/zerolog/log"
)
var SupportedProxies = []string{"nginx", "traefik", "caddy", "envoy"}
@@ -52,7 +52,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
err := c.BindUri(&req)
if err != nil {
log.Error().Err(err).Msg("Failed to bind URI")
tlog.App.Error().Err(err).Msg("Failed to bind URI")
c.JSON(400, gin.H{
"status": 400,
"message": "Bad Request",
@@ -61,7 +61,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
}
if !slices.Contains(SupportedProxies, req.Proxy) {
log.Warn().Str("proxy", req.Proxy).Msg("Invalid proxy")
tlog.App.Warn().Str("proxy", req.Proxy).Msg("Invalid proxy")
c.JSON(400, gin.H{
"status": 400,
"message": "Bad Request",
@@ -73,7 +73,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
// Envoy uses the original client method for the external auth request
// so we allow Any standard HTTP method for /api/auth/envoy
if req.Proxy != "envoy" && c.Request.Method != http.MethodGet {
log.Warn().Str("method", c.Request.Method).Msg("Invalid method for proxy")
tlog.App.Warn().Str("method", c.Request.Method).Msg("Invalid method for proxy")
c.Header("Allow", "GET")
c.JSON(405, gin.H{
"status": 405,
@@ -85,9 +85,9 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
isBrowser := strings.Contains(c.Request.Header.Get("Accept"), "text/html")
if isBrowser {
log.Debug().Msg("Request identified as (most likely) coming from a browser")
tlog.App.Debug().Msg("Request identified as (most likely) coming from a browser")
} else {
log.Debug().Msg("Request identified as (most likely) coming from a non-browser client")
tlog.App.Debug().Msg("Request identified as (most likely) coming from a non-browser client")
}
uri := c.Request.Header.Get("X-Forwarded-Uri")
@@ -98,12 +98,12 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
acls, err := controller.acls.GetAccessControls(host)
if err != nil {
log.Error().Err(err).Msg("Failed to get access controls for resource")
tlog.App.Error().Err(err).Msg("Failed to get access controls for resource")
controller.handleError(c, req, isBrowser)
return
}
log.Trace().Interface("acls", acls).Msg("ACLs for resource")
tlog.App.Trace().Interface("acls", acls).Msg("ACLs for resource")
clientIP := c.ClientIP()
@@ -119,13 +119,13 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
authEnabled, err := controller.auth.IsAuthEnabled(uri, acls.Path)
if err != nil {
log.Error().Err(err).Msg("Failed to check if auth is enabled for resource")
tlog.App.Error().Err(err).Msg("Failed to check if auth is enabled for resource")
controller.handleError(c, req, isBrowser)
return
}
if !authEnabled {
log.Debug().Msg("Authentication disabled for resource, allowing access")
tlog.App.Debug().Msg("Authentication disabled for resource, allowing access")
controller.setHeaders(c, acls)
c.JSON(200, gin.H{
"status": 200,
@@ -149,7 +149,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
})
if err != nil {
log.Error().Err(err).Msg("Failed to encode unauthorized query")
tlog.App.Error().Err(err).Msg("Failed to encode unauthorized query")
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
return
}
@@ -163,7 +163,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
context, err := utils.GetContext(c)
if err != nil {
log.Debug().Msg("No user context found in request, treating as not logged in")
tlog.App.Debug().Msg("No user context found in request, treating as not logged in")
userContext = config.UserContext{
IsLoggedIn: false,
}
@@ -171,10 +171,10 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
userContext = context
}
log.Trace().Interface("context", userContext).Msg("User context from request")
tlog.App.Trace().Interface("context", userContext).Msg("User context from request")
if userContext.Provider == "basic" && userContext.TotpEnabled {
log.Debug().Msg("User has TOTP enabled, denying basic auth access")
tlog.App.Debug().Msg("User has TOTP enabled, denying basic auth access")
userContext.IsLoggedIn = false
}
@@ -182,7 +182,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
userAllowed := controller.auth.IsUserAllowed(c, userContext, acls)
if !userAllowed {
log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User not allowed to access resource")
tlog.App.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User not allowed to access resource")
if req.Proxy == "nginx" || !isBrowser {
c.JSON(403, gin.H{
@@ -197,7 +197,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
})
if err != nil {
log.Error().Err(err).Msg("Failed to encode unauthorized query")
tlog.App.Error().Err(err).Msg("Failed to encode unauthorized query")
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
return
}
@@ -216,7 +216,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
groupOK := controller.auth.IsInOAuthGroup(c, userContext, acls.OAuth.Groups)
if !groupOK {
log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User OAuth groups do not match resource requirements")
tlog.App.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User OAuth groups do not match resource requirements")
if req.Proxy == "nginx" || !isBrowser {
c.JSON(403, gin.H{
@@ -232,7 +232,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
})
if err != nil {
log.Error().Err(err).Msg("Failed to encode unauthorized query")
tlog.App.Error().Err(err).Msg("Failed to encode unauthorized query")
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
return
}
@@ -276,7 +276,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
})
if err != nil {
log.Error().Err(err).Msg("Failed to encode redirect URI query")
tlog.App.Error().Err(err).Msg("Failed to encode redirect URI query")
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
return
}
@@ -290,14 +290,14 @@ func (controller *ProxyController) setHeaders(c *gin.Context, acls config.App) {
headers := utils.ParseHeaders(acls.Response.Headers)
for key, value := range headers {
log.Debug().Str("header", key).Msg("Setting header")
tlog.App.Debug().Str("header", key).Msg("Setting header")
c.Header(key, value)
}
basicPassword := utils.GetSecret(acls.Response.BasicAuth.Password, acls.Response.BasicAuth.PasswordFile)
if acls.Response.BasicAuth.Username != "" && basicPassword != "" {
log.Debug().Str("username", acls.Response.BasicAuth.Username).Msg("Setting basic auth header")
tlog.App.Debug().Str("username", acls.Response.BasicAuth.Username).Msg("Setting basic auth header")
c.Header("Authorization", fmt.Sprintf("Basic %s", utils.GetBasicAuth(acls.Response.BasicAuth.Username, basicPassword)))
}
}

View File

@@ -9,12 +9,15 @@ import (
"github.com/steveiliop56/tinyauth/internal/controller"
"github.com/steveiliop56/tinyauth/internal/repository"
"github.com/steveiliop56/tinyauth/internal/service"
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
"github.com/gin-gonic/gin"
"gotest.tools/v3/assert"
)
func setupProxyController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder, *service.AuthService) {
tlog.NewSimpleLogger().Init()
// Setup
gin.SetMode(gin.TestMode)
router := gin.Default()

View File

@@ -8,10 +8,10 @@ import (
"github.com/steveiliop56/tinyauth/internal/repository"
"github.com/steveiliop56/tinyauth/internal/service"
"github.com/steveiliop56/tinyauth/internal/utils"
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
"github.com/gin-gonic/gin"
"github.com/pquerna/otp/totp"
"github.com/rs/zerolog/log"
)
type LoginRequest struct {
@@ -53,7 +53,7 @@ func (controller *UserController) loginHandler(c *gin.Context) {
err := c.ShouldBindJSON(&req)
if err != nil {
log.Error().Err(err).Msg("Failed to bind JSON")
tlog.App.Error().Err(err).Msg("Failed to bind JSON")
c.JSON(400, gin.H{
"status": 400,
"message": "Bad Request",
@@ -61,12 +61,13 @@ func (controller *UserController) loginHandler(c *gin.Context) {
return
}
log.Debug().Str("username", req.Username).Msg("Login attempt")
tlog.App.Debug().Str("username", req.Username).Msg("Login attempt")
isLocked, remaining := controller.auth.IsAccountLocked(req.Username)
if isLocked {
log.Warn().Str("username", req.Username).Msg("Account is locked due to too many failed login attempts")
tlog.App.Warn().Str("username", req.Username).Msg("Account is locked due to too many failed login attempts")
tlog.AuditLoginFailure(c, req.Username, "username", "account locked")
c.Writer.Header().Add("x-tinyauth-lock-locked", "true")
c.Writer.Header().Add("x-tinyauth-lock-reset", time.Now().Add(time.Duration(remaining)*time.Second).Format(time.RFC3339))
c.JSON(429, gin.H{
@@ -79,8 +80,9 @@ func (controller *UserController) loginHandler(c *gin.Context) {
userSearch := controller.auth.SearchUser(req.Username)
if userSearch.Type == "unknown" {
log.Warn().Str("username", req.Username).Msg("User not found")
tlog.App.Warn().Str("username", req.Username).Msg("User not found")
controller.auth.RecordLoginAttempt(req.Username, false)
tlog.AuditLoginFailure(c, req.Username, "username", "user not found")
c.JSON(401, gin.H{
"status": 401,
"message": "Unauthorized",
@@ -89,8 +91,9 @@ func (controller *UserController) loginHandler(c *gin.Context) {
}
if !controller.auth.VerifyUser(userSearch, req.Password) {
log.Warn().Str("username", req.Username).Msg("Invalid password")
tlog.App.Warn().Str("username", req.Username).Msg("Invalid password")
controller.auth.RecordLoginAttempt(req.Username, false)
tlog.AuditLoginFailure(c, req.Username, "username", "invalid password")
c.JSON(401, gin.H{
"status": 401,
"message": "Unauthorized",
@@ -98,7 +101,8 @@ func (controller *UserController) loginHandler(c *gin.Context) {
return
}
log.Info().Str("username", req.Username).Msg("Login successful")
tlog.App.Info().Str("username", req.Username).Msg("Login successful")
tlog.AuditLoginSuccess(c, req.Username, "username")
controller.auth.RecordLoginAttempt(req.Username, true)
@@ -106,7 +110,7 @@ func (controller *UserController) loginHandler(c *gin.Context) {
user := controller.auth.GetLocalUser(userSearch.Username)
if user.TotpSecret != "" {
log.Debug().Str("username", req.Username).Msg("User has TOTP enabled, requiring TOTP verification")
tlog.App.Debug().Str("username", req.Username).Msg("User has TOTP enabled, requiring TOTP verification")
err := controller.auth.CreateSessionCookie(c, &repository.Session{
Username: user.Username,
@@ -117,7 +121,7 @@ func (controller *UserController) loginHandler(c *gin.Context) {
})
if err != nil {
log.Error().Err(err).Msg("Failed to create session cookie")
tlog.App.Error().Err(err).Msg("Failed to create session cookie")
c.JSON(500, gin.H{
"status": 500,
"message": "Internal Server Error",
@@ -145,7 +149,7 @@ func (controller *UserController) loginHandler(c *gin.Context) {
ldapUser, err := controller.auth.GetLdapUser(userSearch.Username)
if err != nil {
log.Error().Err(err).Str("username", req.Username).Msg("Failed to get LDAP user details")
tlog.App.Error().Err(err).Str("username", req.Username).Msg("Failed to get LDAP user details")
c.JSON(500, gin.H{
"status": 500,
"message": "Internal Server Error",
@@ -156,12 +160,12 @@ func (controller *UserController) loginHandler(c *gin.Context) {
sessionCookie.LdapGroups = strings.Join(ldapUser.Groups, ",")
}
log.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
tlog.App.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
err = controller.auth.CreateSessionCookie(c, &sessionCookie)
if err != nil {
log.Error().Err(err).Msg("Failed to create session cookie")
tlog.App.Error().Err(err).Msg("Failed to create session cookie")
c.JSON(500, gin.H{
"status": 500,
"message": "Internal Server Error",
@@ -176,10 +180,15 @@ func (controller *UserController) loginHandler(c *gin.Context) {
}
func (controller *UserController) logoutHandler(c *gin.Context) {
log.Debug().Msg("Logout request received")
tlog.App.Debug().Msg("Logout request received")
controller.auth.DeleteSessionCookie(c)
context, err := utils.GetContext(c)
if err == nil && context.IsLoggedIn {
tlog.AuditLogout(c, context.Username, context.Provider)
}
c.JSON(200, gin.H{
"status": 200,
"message": "Logout successful",
@@ -191,7 +200,7 @@ func (controller *UserController) totpHandler(c *gin.Context) {
err := c.ShouldBindJSON(&req)
if err != nil {
log.Error().Err(err).Msg("Failed to bind JSON")
tlog.App.Error().Err(err).Msg("Failed to bind JSON")
c.JSON(400, gin.H{
"status": 400,
"message": "Bad Request",
@@ -202,7 +211,7 @@ func (controller *UserController) totpHandler(c *gin.Context) {
context, err := utils.GetContext(c)
if err != nil {
log.Error().Err(err).Msg("Failed to get user context")
tlog.App.Error().Err(err).Msg("Failed to get user context")
c.JSON(500, gin.H{
"status": 500,
"message": "Internal Server Error",
@@ -211,7 +220,7 @@ func (controller *UserController) totpHandler(c *gin.Context) {
}
if !context.TotpPending {
log.Warn().Msg("TOTP attempt without a pending TOTP session")
tlog.App.Warn().Msg("TOTP attempt without a pending TOTP session")
c.JSON(401, gin.H{
"status": 401,
"message": "Unauthorized",
@@ -219,12 +228,12 @@ func (controller *UserController) totpHandler(c *gin.Context) {
return
}
log.Debug().Str("username", context.Username).Msg("TOTP verification attempt")
tlog.App.Debug().Str("username", context.Username).Msg("TOTP verification attempt")
isLocked, remaining := controller.auth.IsAccountLocked(context.Username)
if isLocked {
log.Warn().Str("username", context.Username).Msg("Account is locked due to too many failed TOTP attempts")
tlog.App.Warn().Str("username", context.Username).Msg("Account is locked due to too many failed TOTP attempts")
c.Writer.Header().Add("x-tinyauth-lock-locked", "true")
c.Writer.Header().Add("x-tinyauth-lock-reset", time.Now().Add(time.Duration(remaining)*time.Second).Format(time.RFC3339))
c.JSON(429, gin.H{
@@ -239,8 +248,9 @@ func (controller *UserController) totpHandler(c *gin.Context) {
ok := totp.Validate(req.Code, user.TotpSecret)
if !ok {
log.Warn().Str("username", context.Username).Msg("Invalid TOTP code")
tlog.App.Warn().Str("username", context.Username).Msg("Invalid TOTP code")
controller.auth.RecordLoginAttempt(context.Username, false)
tlog.AuditLoginFailure(c, context.Username, "totp", "invalid totp code")
c.JSON(401, gin.H{
"status": 401,
"message": "Unauthorized",
@@ -248,7 +258,8 @@ func (controller *UserController) totpHandler(c *gin.Context) {
return
}
log.Info().Str("username", context.Username).Msg("TOTP verification successful")
tlog.App.Info().Str("username", context.Username).Msg("TOTP verification successful")
tlog.AuditLoginSuccess(c, context.Username, "totp")
controller.auth.RecordLoginAttempt(context.Username, true)
@@ -259,12 +270,12 @@ func (controller *UserController) totpHandler(c *gin.Context) {
Provider: "username",
}
log.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
tlog.App.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
err = controller.auth.CreateSessionCookie(c, &sessionCookie)
if err != nil {
log.Error().Err(err).Msg("Failed to create session cookie")
tlog.App.Error().Err(err).Msg("Failed to create session cookie")
c.JSON(500, gin.H{
"status": 500,
"message": "Internal Server Error",

View File

@@ -13,6 +13,7 @@ import (
"github.com/steveiliop56/tinyauth/internal/controller"
"github.com/steveiliop56/tinyauth/internal/repository"
"github.com/steveiliop56/tinyauth/internal/service"
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
"github.com/gin-gonic/gin"
"github.com/pquerna/otp/totp"
@@ -23,6 +24,8 @@ var cookieValue string
var totpSecret = "6WFZXPEZRK5MZHHYAFW4DAOUYQMCASBJ"
func setupUserController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder) {
tlog.NewSimpleLogger().Init()
// Setup
gin.SetMode(gin.TestMode)
router := gin.Default()

View File

@@ -8,9 +8,9 @@ import (
"github.com/steveiliop56/tinyauth/internal/config"
"github.com/steveiliop56/tinyauth/internal/service"
"github.com/steveiliop56/tinyauth/internal/utils"
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
)
type ContextMiddlewareConfig struct {
@@ -40,7 +40,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
cookie, err := m.auth.GetSessionCookie(c)
if err != nil {
log.Debug().Err(err).Msg("No valid session cookie found")
tlog.App.Debug().Err(err).Msg("No valid session cookie found")
goto basic
}
@@ -62,7 +62,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
userSearch := m.auth.SearchUser(cookie.Username)
if userSearch.Type == "unknown" || userSearch.Type == "error" {
log.Debug().Msg("User from session cookie not found")
tlog.App.Debug().Msg("User from session cookie not found")
m.auth.DeleteSessionCookie(c)
goto basic
}
@@ -82,13 +82,13 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
_, exists := m.broker.GetService(cookie.Provider)
if !exists {
log.Debug().Msg("OAuth provider from session cookie not found")
tlog.App.Debug().Msg("OAuth provider from session cookie not found")
m.auth.DeleteSessionCookie(c)
goto basic
}
if !m.auth.IsEmailWhitelisted(cookie.Email) {
log.Debug().Msg("Email from session cookie not whitelisted")
tlog.App.Debug().Msg("Email from session cookie not whitelisted")
m.auth.DeleteSessionCookie(c)
goto basic
}
@@ -113,7 +113,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
basic := m.auth.GetBasicAuth(c)
if basic == nil {
log.Debug().Msg("No basic auth provided")
tlog.App.Debug().Msg("No basic auth provided")
c.Next()
return
}
@@ -121,7 +121,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
locked, remaining := m.auth.IsAccountLocked(basic.Username)
if locked {
log.Debug().Msgf("Account for user %s is locked for %d seconds, denying auth", basic.Username, remaining)
tlog.App.Debug().Msgf("Account for user %s is locked for %d seconds, denying auth", basic.Username, remaining)
c.Writer.Header().Add("x-tinyauth-lock-locked", "true")
c.Writer.Header().Add("x-tinyauth-lock-reset", time.Now().Add(time.Duration(remaining)*time.Second).Format(time.RFC3339))
c.Next()
@@ -132,14 +132,14 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
if userSearch.Type == "unknown" || userSearch.Type == "error" {
m.auth.RecordLoginAttempt(basic.Username, false)
log.Debug().Msg("User from basic auth not found")
tlog.App.Debug().Msg("User from basic auth not found")
c.Next()
return
}
if !m.auth.VerifyUser(userSearch, basic.Password) {
m.auth.RecordLoginAttempt(basic.Username, false)
log.Debug().Msg("Invalid password for basic auth user")
tlog.App.Debug().Msg("Invalid password for basic auth user")
c.Next()
return
}
@@ -148,7 +148,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
switch userSearch.Type {
case "local":
log.Debug().Msg("Basic auth user is local")
tlog.App.Debug().Msg("Basic auth user is local")
user := m.auth.GetLocalUser(basic.Username)

View File

@@ -5,7 +5,7 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
)
var (
@@ -49,7 +49,7 @@ func (m *ZerologMiddleware) Middleware() gin.HandlerFunc {
latency := time.Since(tStart).String()
subLogger := log.With().Str("method", method).
subLogger := tlog.HTTP.With().Str("method", method).
Str("path", path).
Str("address", address).
Str("client_ip", clientIP).

View File

@@ -4,8 +4,8 @@ import (
"errors"
"strings"
"github.com/rs/zerolog/log"
"github.com/steveiliop56/tinyauth/internal/config"
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
)
type AccessControlsService struct {
@@ -27,12 +27,12 @@ func (acls *AccessControlsService) Init() error {
func (acls *AccessControlsService) lookupStaticACLs(domain string) (config.App, error) {
for app, config := range acls.static {
if config.Config.Domain == domain {
log.Debug().Str("name", app).Msg("Found matching container by domain")
tlog.App.Debug().Str("name", app).Msg("Found matching container by domain")
return config, nil
}
if strings.SplitN(domain, ".", 2)[0] == app {
log.Debug().Str("name", app).Msg("Found matching container by app name")
tlog.App.Debug().Str("name", app).Msg("Found matching container by app name")
return config, nil
}
}
@@ -44,11 +44,11 @@ func (acls *AccessControlsService) GetAccessControls(domain string) (config.App,
app, err := acls.lookupStaticACLs(domain)
if err == nil {
log.Debug().Msg("Using ACls from static configuration")
tlog.App.Debug().Msg("Using ACls from static configuration")
return app, nil
}
// Fallback to Docker labels
log.Debug().Msg("Falling back to Docker labels for ACLs")
tlog.App.Debug().Msg("Falling back to Docker labels for ACLs")
return acls.docker.GetLabels(domain)
}

View File

@@ -12,10 +12,10 @@ import (
"github.com/steveiliop56/tinyauth/internal/config"
"github.com/steveiliop56/tinyauth/internal/repository"
"github.com/steveiliop56/tinyauth/internal/utils"
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/rs/zerolog/log"
"golang.org/x/crypto/bcrypt"
)
@@ -73,7 +73,7 @@ func (auth *AuthService) SearchUser(username string) config.UserSearch {
userDN, err := auth.ldap.GetUserDN(username)
if err != nil {
log.Warn().Err(err).Str("username", username).Msg("Failed to search for user in LDAP")
tlog.App.Warn().Err(err).Str("username", username).Msg("Failed to search for user in LDAP")
return config.UserSearch{
Type: "error",
}
@@ -99,24 +99,24 @@ func (auth *AuthService) VerifyUser(search config.UserSearch, password string) b
if auth.ldap != nil {
err := auth.ldap.Bind(search.Username, password)
if err != nil {
log.Warn().Err(err).Str("username", search.Username).Msg("Failed to bind to LDAP")
tlog.App.Warn().Err(err).Str("username", search.Username).Msg("Failed to bind to LDAP")
return false
}
err = auth.ldap.BindService(true)
if err != nil {
log.Error().Err(err).Msg("Failed to rebind with service account after user authentication")
tlog.App.Error().Err(err).Msg("Failed to rebind with service account after user authentication")
return false
}
return true
}
default:
log.Debug().Str("type", search.Type).Msg("Unknown user type for authentication")
tlog.App.Debug().Str("type", search.Type).Msg("Unknown user type for authentication")
return false
}
log.Warn().Str("username", search.Username).Msg("User authentication failed")
tlog.App.Warn().Str("username", search.Username).Msg("User authentication failed")
return false
}
@@ -127,7 +127,7 @@ func (auth *AuthService) GetLocalUser(username string) config.User {
}
}
log.Warn().Str("username", username).Msg("Local user not found")
tlog.App.Warn().Str("username", username).Msg("Local user not found")
return config.User{}
}
@@ -195,7 +195,7 @@ func (auth *AuthService) RecordLoginAttempt(identifier string, success bool) {
if attempt.FailedAttempts >= auth.config.LoginMaxRetries {
attempt.LockedUntil = time.Now().Add(time.Duration(auth.config.LoginTimeout) * time.Second)
log.Warn().Str("identifier", identifier).Int("timeout", auth.config.LoginTimeout).Msg("Account locked due to too many failed login attempts")
tlog.App.Warn().Str("identifier", identifier).Int("timeout", auth.config.LoginTimeout).Msg("Account locked due to too many failed login attempts")
}
}
@@ -292,7 +292,7 @@ func (auth *AuthService) RefreshSessionCookie(c *gin.Context) error {
}
c.SetCookie(auth.config.SessionCookieName, cookie, int(newExpiry-currentTime), "/", fmt.Sprintf(".%s", auth.config.CookieDomain), auth.config.SecureCookie, true)
log.Trace().Str("username", session.Username).Msg("Session cookie refreshed")
tlog.App.Trace().Str("username", session.Username).Msg("Session cookie refreshed")
return nil
}
@@ -337,7 +337,7 @@ func (auth *AuthService) GetSessionCookie(c *gin.Context) (repository.Session, e
if currentTime-session.CreatedAt > int64(auth.config.SessionMaxLifetime) {
err = auth.queries.DeleteSession(c, cookie)
if err != nil {
log.Error().Err(err).Msg("Failed to delete session exceeding max lifetime")
tlog.App.Error().Err(err).Msg("Failed to delete session exceeding max lifetime")
}
return repository.Session{}, fmt.Errorf("session expired due to max lifetime exceeded")
}
@@ -346,7 +346,7 @@ func (auth *AuthService) GetSessionCookie(c *gin.Context) (repository.Session, e
if currentTime > session.Expiry {
err = auth.queries.DeleteSession(c, cookie)
if err != nil {
log.Error().Err(err).Msg("Failed to delete expired session")
tlog.App.Error().Err(err).Msg("Failed to delete expired session")
}
return repository.Session{}, fmt.Errorf("session expired")
}
@@ -371,18 +371,18 @@ func (auth *AuthService) UserAuthConfigured() bool {
func (auth *AuthService) IsUserAllowed(c *gin.Context, context config.UserContext, acls config.App) bool {
if context.OAuth {
log.Debug().Msg("Checking OAuth whitelist")
tlog.App.Debug().Msg("Checking OAuth whitelist")
return utils.CheckFilter(acls.OAuth.Whitelist, context.Email)
}
if acls.Users.Block != "" {
log.Debug().Msg("Checking blocked users")
tlog.App.Debug().Msg("Checking blocked users")
if utils.CheckFilter(acls.Users.Block, context.Username) {
return false
}
}
log.Debug().Msg("Checking users")
tlog.App.Debug().Msg("Checking users")
return utils.CheckFilter(acls.Users.Allow, context.Username)
}
@@ -393,19 +393,19 @@ func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context config.UserConte
for id := range config.OverrideProviders {
if context.Provider == id {
log.Info().Str("provider", id).Msg("OAuth groups not supported for this provider")
tlog.App.Info().Str("provider", id).Msg("OAuth groups not supported for this provider")
return true
}
}
for userGroup := range strings.SplitSeq(context.OAuthGroups, ",") {
if utils.CheckFilter(requiredGroups, strings.TrimSpace(userGroup)) {
log.Trace().Str("group", userGroup).Str("required", requiredGroups).Msg("User group matched")
tlog.App.Trace().Str("group", userGroup).Str("required", requiredGroups).Msg("User group matched")
return true
}
}
log.Debug().Msg("No groups matched")
tlog.App.Debug().Msg("No groups matched")
return false
}
@@ -442,7 +442,7 @@ func (auth *AuthService) IsAuthEnabled(uri string, path config.AppPath) (bool, e
func (auth *AuthService) GetBasicAuth(c *gin.Context) *config.User {
username, password, ok := c.Request.BasicAuth()
if !ok {
log.Debug().Msg("No basic auth provided")
tlog.App.Debug().Msg("No basic auth provided")
return nil
}
return &config.User{
@@ -459,11 +459,11 @@ func (auth *AuthService) CheckIP(acls config.AppIP, ip string) bool {
for _, blocked := range blockedIps {
res, err := utils.FilterIP(blocked, ip)
if err != nil {
log.Warn().Err(err).Str("item", blocked).Msg("Invalid IP/CIDR in block list")
tlog.App.Warn().Err(err).Str("item", blocked).Msg("Invalid IP/CIDR in block list")
continue
}
if res {
log.Debug().Str("ip", ip).Str("item", blocked).Msg("IP is in blocked list, denying access")
tlog.App.Debug().Str("ip", ip).Str("item", blocked).Msg("IP is in blocked list, denying access")
return false
}
}
@@ -471,21 +471,21 @@ func (auth *AuthService) CheckIP(acls config.AppIP, ip string) bool {
for _, allowed := range allowedIPs {
res, err := utils.FilterIP(allowed, ip)
if err != nil {
log.Warn().Err(err).Str("item", allowed).Msg("Invalid IP/CIDR in allow list")
tlog.App.Warn().Err(err).Str("item", allowed).Msg("Invalid IP/CIDR in allow list")
continue
}
if res {
log.Debug().Str("ip", ip).Str("item", allowed).Msg("IP is in allowed list, allowing access")
tlog.App.Debug().Str("ip", ip).Str("item", allowed).Msg("IP is in allowed list, allowing access")
return true
}
}
if len(allowedIPs) > 0 {
log.Debug().Str("ip", ip).Msg("IP not in allow list, denying access")
tlog.App.Debug().Str("ip", ip).Msg("IP not in allow list, denying access")
return false
}
log.Debug().Str("ip", ip).Msg("IP not in allow or block list, allowing by default")
tlog.App.Debug().Str("ip", ip).Msg("IP not in allow or block list, allowing by default")
return true
}
@@ -493,15 +493,15 @@ func (auth *AuthService) IsBypassedIP(acls config.AppIP, ip string) bool {
for _, bypassed := range acls.Bypass {
res, err := utils.FilterIP(bypassed, ip)
if err != nil {
log.Warn().Err(err).Str("item", bypassed).Msg("Invalid IP/CIDR in bypass list")
tlog.App.Warn().Err(err).Str("item", bypassed).Msg("Invalid IP/CIDR in bypass list")
continue
}
if res {
log.Debug().Str("ip", ip).Str("item", bypassed).Msg("IP is in bypass list, allowing access")
tlog.App.Debug().Str("ip", ip).Str("item", bypassed).Msg("IP is in bypass list, allowing access")
return true
}
}
log.Debug().Str("ip", ip).Msg("IP not in bypass list, continuing with authentication")
tlog.App.Debug().Str("ip", ip).Msg("IP not in bypass list, continuing with authentication")
return false
}

View File

@@ -6,10 +6,10 @@ import (
"github.com/steveiliop56/tinyauth/internal/config"
"github.com/steveiliop56/tinyauth/internal/utils/decoders"
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
container "github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/rs/zerolog/log"
)
type DockerService struct {
@@ -37,7 +37,7 @@ func (docker *DockerService) Init() error {
_, err = docker.client.Ping(docker.context)
if err != nil {
log.Debug().Err(err).Msg("Docker not connected")
tlog.App.Debug().Err(err).Msg("Docker not connected")
docker.isConnected = false
docker.client = nil
docker.context = nil
@@ -45,7 +45,7 @@ func (docker *DockerService) Init() error {
}
docker.isConnected = true
log.Debug().Msg("Docker connected")
tlog.App.Debug().Msg("Docker connected")
return nil
}
@@ -68,7 +68,7 @@ func (docker *DockerService) inspectContainer(containerId string) (container.Ins
func (docker *DockerService) GetLabels(appDomain string) (config.App, error) {
if !docker.isConnected {
log.Debug().Msg("Docker not connected, returning empty labels")
tlog.App.Debug().Msg("Docker not connected, returning empty labels")
return config.App{}, nil
}
@@ -90,17 +90,17 @@ func (docker *DockerService) GetLabels(appDomain string) (config.App, error) {
for appName, appLabels := range labels.Apps {
if appLabels.Config.Domain == appDomain {
log.Debug().Str("id", inspect.ID).Str("name", inspect.Name).Msg("Found matching container by domain")
tlog.App.Debug().Str("id", inspect.ID).Str("name", inspect.Name).Msg("Found matching container by domain")
return appLabels, nil
}
if strings.SplitN(appDomain, ".", 2)[0] == appName {
log.Debug().Str("id", inspect.ID).Str("name", inspect.Name).Msg("Found matching container by app name")
tlog.App.Debug().Str("id", inspect.ID).Str("name", inspect.Name).Msg("Found matching container by app name")
return appLabels, nil
}
}
}
log.Debug().Msg("No matching container found, returning empty labels")
tlog.App.Debug().Msg("No matching container found, returning empty labels")
return config.App{}, nil
}

View File

@@ -12,8 +12,8 @@ import (
"time"
"github.com/steveiliop56/tinyauth/internal/config"
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
"github.com/rs/zerolog/log"
"golang.org/x/oauth2"
)
@@ -117,7 +117,7 @@ func (generic *GenericOAuthService) Userinfo() (config.Claims, error) {
return user, err
}
log.Trace().Str("body", string(body)).Msg("Userinfo response body")
tlog.App.Trace().Str("body", string(body)).Msg("Userinfo response body")
err = json.Unmarshal(body, &user)
if err != nil {

View File

@@ -11,7 +11,7 @@ import (
"github.com/cenkalti/backoff/v5"
ldapgo "github.com/go-ldap/ldap/v3"
"github.com/rs/zerolog/log"
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
)
type LdapServiceConfig struct {
@@ -46,7 +46,7 @@ func (ldap *LdapService) Init() error {
return fmt.Errorf("failed to initialize LDAP with mTLS authentication: %w", err)
}
ldap.cert = &cert
log.Info().Msg("Using LDAP with mTLS authentication")
tlog.App.Info().Msg("Using LDAP with mTLS authentication")
// TODO: Add optional extra CA certificates, instead of `InsecureSkipVerify`
/*
@@ -68,12 +68,12 @@ func (ldap *LdapService) Init() error {
for range time.Tick(time.Duration(5) * time.Minute) {
err := ldap.heartbeat()
if err != nil {
log.Error().Err(err).Msg("LDAP connection heartbeat failed")
tlog.App.Error().Err(err).Msg("LDAP connection heartbeat failed")
if reconnectErr := ldap.reconnect(); reconnectErr != nil {
log.Error().Err(reconnectErr).Msg("Failed to reconnect to LDAP server")
tlog.App.Error().Err(reconnectErr).Msg("Failed to reconnect to LDAP server")
continue
}
log.Info().Msg("Successfully reconnected to LDAP server")
tlog.App.Info().Msg("Successfully reconnected to LDAP server")
}
}
}()
@@ -212,7 +212,7 @@ func (ldap *LdapService) Bind(userDN string, password string) error {
}
func (ldap *LdapService) heartbeat() error {
log.Debug().Msg("Performing LDAP connection heartbeat")
tlog.App.Debug().Msg("Performing LDAP connection heartbeat")
searchRequest := ldapgo.NewSearchRequest(
"",
@@ -234,7 +234,7 @@ func (ldap *LdapService) heartbeat() error {
}
func (ldap *LdapService) reconnect() error {
log.Info().Msg("Reconnecting to LDAP server")
tlog.App.Info().Msg("Reconnecting to LDAP server")
exp := backoff.NewExponentialBackOff()
exp.InitialInterval = 500 * time.Millisecond

View File

@@ -4,8 +4,8 @@ import (
"errors"
"github.com/steveiliop56/tinyauth/internal/config"
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
"github.com/rs/zerolog/log"
"golang.org/x/exp/slices"
)
@@ -49,10 +49,10 @@ func (broker *OAuthBrokerService) Init() error {
for name, service := range broker.services {
err := service.Init()
if err != nil {
log.Error().Err(err).Msgf("Failed to initialize OAuth service: %T", name)
tlog.App.Error().Err(err).Msgf("Failed to initialize OAuth service: %s", name)
return err
}
log.Info().Str("service", name).Msg("Initialized OAuth service")
tlog.App.Info().Str("service", name).Msg("Initialized OAuth service")
}
return nil

View File

@@ -0,0 +1,39 @@
package tlog
import "github.com/gin-gonic/gin"
// functions here use CallerSkipFrame to ensure correct caller info is logged
func AuditLoginSuccess(c *gin.Context, username, provider string) {
Audit.Info().
CallerSkipFrame(1).
Str("event", "login").
Str("result", "success").
Str("username", username).
Str("provider", provider).
Str("ip", c.ClientIP()).
Send()
}
func AuditLoginFailure(c *gin.Context, username, provider string, reason string) {
Audit.Warn().
CallerSkipFrame(1).
Str("event", "login").
Str("result", "failure").
Str("username", username).
Str("provider", provider).
Str("ip", c.ClientIP()).
Str("reason", reason).
Send()
}
func AuditLogout(c *gin.Context, username, provider string) {
Audit.Info().
CallerSkipFrame(1).
Str("event", "logout").
Str("result", "success").
Str("username", username).
Str("provider", provider).
Str("ip", c.ClientIP()).
Send()
}

View File

@@ -0,0 +1,86 @@
package tlog
import (
"os"
"strings"
"time"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/steveiliop56/tinyauth/internal/config"
)
type Logger struct {
Audit zerolog.Logger
HTTP zerolog.Logger
App zerolog.Logger
}
var (
Audit zerolog.Logger
HTTP zerolog.Logger
App zerolog.Logger
)
func NewLogger(cfg config.LogConfig) *Logger {
baseLogger := log.With().
Timestamp().
Caller().
Logger().
Level(parseLogLevel(cfg.Level))
if !cfg.Json {
baseLogger = baseLogger.Output(zerolog.ConsoleWriter{
Out: os.Stderr,
TimeFormat: time.RFC3339,
})
}
return &Logger{
Audit: createLogger("audit", cfg.Streams.Audit, baseLogger),
HTTP: createLogger("http", cfg.Streams.HTTP, baseLogger),
App: createLogger("app", cfg.Streams.App, baseLogger),
}
}
func NewSimpleLogger() *Logger {
return NewLogger(config.LogConfig{
Level: "info",
Json: false,
Streams: config.LogStreams{
HTTP: config.LogStreamConfig{Enabled: true},
App: config.LogStreamConfig{Enabled: true},
Audit: config.LogStreamConfig{Enabled: false},
},
})
}
func (l *Logger) Init() {
Audit = l.Audit
HTTP = l.HTTP
App = l.App
}
func createLogger(component string, streamCfg config.LogStreamConfig, baseLogger zerolog.Logger) zerolog.Logger {
if !streamCfg.Enabled {
return zerolog.Nop()
}
subLogger := baseLogger.With().Str("log_stream", component).Logger()
// override level if specified, otherwise use base level
if streamCfg.Level != "" {
subLogger = subLogger.Level(parseLogLevel(streamCfg.Level))
}
return subLogger
}
func parseLogLevel(level string) zerolog.Level {
if level == "" {
return zerolog.InfoLevel
}
parsedLevel, err := zerolog.ParseLevel(strings.ToLower(level))
if err != nil {
log.Warn().Err(err).Str("level", level).Msg("Invalid log level, defaulting to info")
parsedLevel = zerolog.InfoLevel
}
return parsedLevel
}

View File

@@ -0,0 +1,93 @@
package tlog_test
import (
"bytes"
"encoding/json"
"testing"
"github.com/steveiliop56/tinyauth/internal/config"
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
"github.com/rs/zerolog"
"gotest.tools/v3/assert"
)
func TestNewLogger(t *testing.T) {
cfg := config.LogConfig{
Level: "debug",
Json: true,
Streams: config.LogStreams{
HTTP: config.LogStreamConfig{Enabled: true, Level: "info"},
App: config.LogStreamConfig{Enabled: true, Level: ""},
Audit: config.LogStreamConfig{Enabled: false, Level: ""},
},
}
logger := tlog.NewLogger(cfg)
assert.Assert(t, logger != nil)
assert.Assert(t, logger.HTTP.GetLevel() == zerolog.InfoLevel)
assert.Assert(t, logger.App.GetLevel() == zerolog.DebugLevel)
assert.Assert(t, logger.Audit.GetLevel() == zerolog.Disabled)
}
func TestNewSimpleLogger(t *testing.T) {
logger := tlog.NewSimpleLogger()
assert.Assert(t, logger != nil)
assert.Assert(t, logger.HTTP.GetLevel() == zerolog.InfoLevel)
assert.Assert(t, logger.App.GetLevel() == zerolog.InfoLevel)
assert.Assert(t, logger.Audit.GetLevel() == zerolog.Disabled)
}
func TestLoggerInit(t *testing.T) {
logger := tlog.NewSimpleLogger()
logger.Init()
assert.Assert(t, tlog.App.GetLevel() != zerolog.Disabled)
}
func TestLoggerWithDisabledStreams(t *testing.T) {
cfg := config.LogConfig{
Level: "info",
Json: false,
Streams: config.LogStreams{
HTTP: config.LogStreamConfig{Enabled: false},
App: config.LogStreamConfig{Enabled: false},
Audit: config.LogStreamConfig{Enabled: false},
},
}
logger := tlog.NewLogger(cfg)
assert.Assert(t, logger.HTTP.GetLevel() == zerolog.Disabled)
assert.Assert(t, logger.App.GetLevel() == zerolog.Disabled)
assert.Assert(t, logger.Audit.GetLevel() == zerolog.Disabled)
}
func TestLogStreamField(t *testing.T) {
var buf bytes.Buffer
cfg := config.LogConfig{
Level: "info",
Json: true,
Streams: config.LogStreams{
HTTP: config.LogStreamConfig{Enabled: true},
App: config.LogStreamConfig{Enabled: true},
Audit: config.LogStreamConfig{Enabled: true},
},
}
logger := tlog.NewLogger(cfg)
// Override output for HTTP logger to capture output
logger.HTTP = logger.HTTP.Output(&buf)
logger.HTTP.Info().Msg("test message")
var logEntry map[string]interface{}
err := json.Unmarshal(buf.Bytes(), &logEntry)
assert.NilError(t, err)
assert.Equal(t, "http", logEntry["log_stream"])
assert.Equal(t, "test message", logEntry["message"])
}