mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-01-15 20:02:30 +00:00
Compare commits
10 Commits
nightly
...
feat/ldap-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4917202879 | ||
|
|
6597bea1b2 | ||
|
|
3f69509622 | ||
|
|
c0dd432029 | ||
|
|
a0ef7b80d8 | ||
|
|
53bd413046 | ||
|
|
1b2bf3902c | ||
|
|
467c580ec4 | ||
|
|
98c0d7be24 | ||
|
|
caf993a738 |
17
.env.example
17
.env.example
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
# The base URL where Tinyauth is accessible
|
# The base URL where Tinyauth is accessible
|
||||||
TINYAUTH_APPURL="https://auth.example.com"
|
TINYAUTH_APPURL="https://auth.example.com"
|
||||||
# Log level: trace, debug, info, warn, error
|
|
||||||
TINYAUTH_LOGLEVEL="info"
|
|
||||||
# Directory for static resources
|
# Directory for static resources
|
||||||
TINYAUTH_RESOURCESDIR="/data/resources"
|
TINYAUTH_RESOURCESDIR="/data/resources"
|
||||||
# Path to SQLite database file
|
# Path to SQLite database file
|
||||||
@@ -14,8 +12,21 @@ TINYAUTH_DISABLEANALYTICS="false"
|
|||||||
TINYAUTH_DISABLERESOURCES="false"
|
TINYAUTH_DISABLERESOURCES="false"
|
||||||
# Disable UI warning messages
|
# Disable UI warning messages
|
||||||
TINYAUTH_DISABLEUIWARNINGS="false"
|
TINYAUTH_DISABLEUIWARNINGS="false"
|
||||||
|
|
||||||
|
# Logging Configuration
|
||||||
|
|
||||||
|
# Log level: trace, debug, info, warn, error
|
||||||
|
TINYAUTH_LOG_LEVEL="info"
|
||||||
# Enable JSON formatted logs
|
# 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
|
# Server Configuration
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,10 @@ package main
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/charmbracelet/huh"
|
"github.com/charmbracelet/huh"
|
||||||
"github.com/rs/zerolog"
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/traefik/paerser/cli"
|
"github.com/traefik/paerser/cli"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
@@ -43,7 +40,7 @@ func createUserCmd() *cli.Command {
|
|||||||
Configuration: tCfg,
|
Configuration: tCfg,
|
||||||
Resources: loaders,
|
Resources: loaders,
|
||||||
Run: func(_ []string) error {
|
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 {
|
if tCfg.Interactive {
|
||||||
form := huh.NewForm(
|
form := huh.NewForm(
|
||||||
@@ -77,7 +74,7 @@ func createUserCmd() *cli.Command {
|
|||||||
return errors.New("username and password cannot be empty")
|
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)
|
passwd, err := bcrypt.GenerateFromPassword([]byte(tCfg.Password), bcrypt.DefaultCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -90,7 +87,7 @@ func createUserCmd() *cli.Command {
|
|||||||
passwdStr = strings.ReplaceAll(passwdStr, "$", "$$")
|
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
|
return nil
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,15 +5,13 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/charmbracelet/huh"
|
"github.com/charmbracelet/huh"
|
||||||
"github.com/mdp/qrterminal/v3"
|
"github.com/mdp/qrterminal/v3"
|
||||||
"github.com/pquerna/otp/totp"
|
"github.com/pquerna/otp/totp"
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/traefik/paerser/cli"
|
"github.com/traefik/paerser/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -42,7 +40,7 @@ func generateTotpCmd() *cli.Command {
|
|||||||
Configuration: tCfg,
|
Configuration: tCfg,
|
||||||
Resources: loaders,
|
Resources: loaders,
|
||||||
Run: func(_ []string) error {
|
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 {
|
if tCfg.Interactive {
|
||||||
form := huh.NewForm(
|
form := huh.NewForm(
|
||||||
@@ -91,9 +89,9 @@ func generateTotpCmd() *cli.Command {
|
|||||||
|
|
||||||
secret := key.Secret()
|
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{
|
config := qrterminal.Config{
|
||||||
Level: qrterminal.L,
|
Level: qrterminal.L,
|
||||||
@@ -112,7 +110,7 @@ func generateTotpCmd() *cli.Command {
|
|||||||
user.Password = strings.ReplaceAll(user.Password, "$", "$$")
|
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
|
return nil
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/traefik/paerser/cli"
|
"github.com/traefik/paerser/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,7 +26,7 @@ func healthcheckCmd() *cli.Command {
|
|||||||
Resources: nil,
|
Resources: nil,
|
||||||
AllowArg: true,
|
AllowArg: true,
|
||||||
Run: func(args []string) error {
|
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")
|
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")
|
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{
|
client := http.Client{
|
||||||
Timeout: 30 * time.Second,
|
Timeout: 30 * time.Second,
|
||||||
@@ -77,7 +76,7 @@ func healthcheckCmd() *cli.Command {
|
|||||||
return fmt.Errorf("failed to decode response: %w", err)
|
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
|
return nil
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,22 +2,18 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/bootstrap"
|
"github.com/steveiliop56/tinyauth/internal/bootstrap"
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/loaders"
|
"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/rs/zerolog/log"
|
||||||
"github.com/traefik/paerser/cli"
|
"github.com/traefik/paerser/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewTinyauthCmdConfiguration() *config.Config {
|
func NewTinyauthCmdConfiguration() *config.Config {
|
||||||
return &config.Config{
|
return &config.Config{
|
||||||
LogLevel: "info",
|
|
||||||
ResourcesDir: "./resources",
|
ResourcesDir: "./resources",
|
||||||
DatabasePath: "./tinyauth.db",
|
DatabasePath: "./tinyauth.db",
|
||||||
Server: config.ServerConfig{
|
Server: config.ServerConfig{
|
||||||
@@ -39,6 +35,24 @@ func NewTinyauthCmdConfiguration() *config.Config {
|
|||||||
Insecure: false,
|
Insecure: false,
|
||||||
SearchFilter: "(uid=%s)",
|
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{
|
Experimental: config.ExperimentalConfig{
|
||||||
ConfigFile: "",
|
ConfigFile: "",
|
||||||
},
|
},
|
||||||
@@ -102,25 +116,14 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runCmd(cfg config.Config) error {
|
func runCmd(cfg config.Config) error {
|
||||||
logLevel, err := zerolog.ParseLevel(strings.ToLower(cfg.LogLevel))
|
logger := tlog.NewLogger(cfg.Log)
|
||||||
|
logger.Init()
|
||||||
|
|
||||||
if err != nil {
|
tlog.App.Info().Str("version", config.Version).Msg("Starting tinyauth")
|
||||||
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")
|
|
||||||
|
|
||||||
app := bootstrap.NewBootstrapApp(cfg)
|
app := bootstrap.NewBootstrapApp(cfg)
|
||||||
|
|
||||||
err = app.Setup()
|
err := app.Setup()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to bootstrap app: %w", err)
|
return fmt.Errorf("failed to bootstrap app: %w", err)
|
||||||
|
|||||||
@@ -3,15 +3,12 @@ package main
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/charmbracelet/huh"
|
"github.com/charmbracelet/huh"
|
||||||
"github.com/pquerna/otp/totp"
|
"github.com/pquerna/otp/totp"
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/traefik/paerser/cli"
|
"github.com/traefik/paerser/cli"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
@@ -47,7 +44,7 @@ func verifyUserCmd() *cli.Command {
|
|||||||
Configuration: tCfg,
|
Configuration: tCfg,
|
||||||
Resources: loaders,
|
Resources: loaders,
|
||||||
Run: func(_ []string) error {
|
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 {
|
if tCfg.Interactive {
|
||||||
form := huh.NewForm(
|
form := huh.NewForm(
|
||||||
@@ -101,9 +98,9 @@ func verifyUserCmd() *cli.Command {
|
|||||||
|
|
||||||
if user.TotpSecret == "" {
|
if user.TotpSecret == "" {
|
||||||
if tCfg.Totp != "" {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +110,7 @@ func verifyUserCmd() *cli.Command {
|
|||||||
return fmt.Errorf("TOTP code incorrect")
|
return fmt.Errorf("TOTP code incorrect")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info().Msg("User verified")
|
tlog.App.Info().Msg("User verified")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
# The base URL where Tinyauth is accessible
|
# The base URL where Tinyauth is accessible
|
||||||
appUrl: "https://auth.example.com"
|
appUrl: "https://auth.example.com"
|
||||||
# Log level: trace, debug, info, warn, error
|
|
||||||
logLevel: "info"
|
|
||||||
# Directory for static resources
|
# Directory for static resources
|
||||||
resourcesDir: "./resources"
|
resourcesDir: "./resources"
|
||||||
# Path to SQLite database file
|
# Path to SQLite database file
|
||||||
@@ -14,8 +12,22 @@ disableAnalytics: false
|
|||||||
disableResources: false
|
disableResources: false
|
||||||
# Disable UI warning messages
|
# Disable UI warning messages
|
||||||
disableUIWarnings: false
|
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 Configuration
|
||||||
server:
|
server:
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@tanstack/react-query": "^5.90.16",
|
"@tanstack/react-query": "^5.90.17",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -24,8 +24,8 @@
|
|||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.3",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.3",
|
||||||
"react-hook-form": "^7.71.0",
|
"react-hook-form": "^7.71.1",
|
||||||
"react-i18next": "^16.5.2",
|
"react-i18next": "^16.5.3",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-router": "^7.12.0",
|
"react-router": "^7.12.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
"@tanstack/eslint-plugin-query": "^5.91.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": "^19.2.8",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^5.1.2",
|
"@vitejs/plugin-react": "^5.1.2",
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.4.26",
|
"eslint-plugin-react-refresh": "^0.4.26",
|
||||||
"globals": "^17.0.0",
|
"globals": "^17.0.0",
|
||||||
"prettier": "3.7.4",
|
"prettier": "3.8.0",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"typescript-eslint": "^8.53.0",
|
"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/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=="],
|
"@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/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=="],
|
"@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=="],
|
"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=="],
|
"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-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=="],
|
"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=="],
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@tanstack/react-query": "^5.90.16",
|
"@tanstack/react-query": "^5.90.17",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -30,8 +30,8 @@
|
|||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.3",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.3",
|
||||||
"react-hook-form": "^7.71.0",
|
"react-hook-form": "^7.71.1",
|
||||||
"react-i18next": "^16.5.2",
|
"react-i18next": "^16.5.3",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-router": "^7.12.0",
|
"react-router": "^7.12.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
"@tanstack/eslint-plugin-query": "^5.91.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": "^19.2.8",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^5.1.2",
|
"@vitejs/plugin-react": "^5.1.2",
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.4.26",
|
"eslint-plugin-react-refresh": "^0.4.26",
|
||||||
"globals": "^17.0.0",
|
"globals": "^17.0.0",
|
||||||
"prettier": "3.7.4",
|
"prettier": "3.8.0",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"typescript-eslint": "^8.53.0",
|
"typescript-eslint": "^8.53.0",
|
||||||
|
|||||||
8
go.mod
8
go.mod
@@ -21,10 +21,10 @@ require (
|
|||||||
github.com/traefik/paerser v0.2.2
|
github.com/traefik/paerser v0.2.2
|
||||||
github.com/weppos/publicsuffix-go v0.50.2
|
github.com/weppos/publicsuffix-go v0.50.2
|
||||||
golang.org/x/crypto v0.47.0
|
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
|
golang.org/x/oauth2 v0.34.0
|
||||||
gotest.tools/v3 v3.5.2
|
gotest.tools/v3 v3.5.2
|
||||||
modernc.org/sqlite v1.43.0
|
modernc.org/sqlite v1.44.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -91,7 +91,7 @@ require (
|
|||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/muesli/termenv v0.16.0 // 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/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
@@ -119,7 +119,7 @@ require (
|
|||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.9 // indirect
|
google.golang.org/protobuf v1.36.9 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // 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/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
rsc.io/qr v0.2.0 // indirect
|
rsc.io/qr v0.2.0 // indirect
|
||||||
|
|||||||
28
go.sum
28
go.sum
@@ -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/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 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
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.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 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
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/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 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
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 v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
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 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
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=
|
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.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 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
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-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||||
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/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.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 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
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=
|
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 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
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.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||||
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||||
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
|
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
|
||||||
modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q=
|
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 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||||
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
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 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
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 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
|
modernc.org/libc v1.67.4 h1:zZGmCMUVPORtKv95c2ReQN5VDjvkoRm9GWPTEPuvlWg=
|
||||||
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
|
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 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
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/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
modernc.org/sqlite v1.43.0 h1:8YqiFx3G1VhHTXO2Q00bl1Wz9KhS9Q5okwfp9Y97VnA=
|
modernc.org/sqlite v1.44.0 h1:YjCKJnzZde2mLVy0cMKTSL4PxCmbIguOq9lGp8ZvGOc=
|
||||||
modernc.org/sqlite v1.43.0/go.mod h1:+VkC6v3pLOAE0A0uVucQEcbVW0I5nHCeDaBf+DpsQT8=
|
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 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
|||||||
1
internal/assets/migrations/000005_ldap_groups.down.sql
Normal file
1
internal/assets/migrations/000005_ldap_groups.down.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "sessions" DROP COLUMN "ldap_groups";
|
||||||
1
internal/assets/migrations/000005_ldap_groups.up.sql
Normal file
1
internal/assets/migrations/000005_ldap_groups.up.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "sessions" ADD COLUMN "ldap_groups" TEXT;
|
||||||
@@ -16,8 +16,7 @@ import (
|
|||||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||||
"github.com/steveiliop56/tinyauth/internal/repository"
|
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type BootstrapApp struct {
|
type BootstrapApp struct {
|
||||||
@@ -103,13 +102,13 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
app.context.redirectCookieName = fmt.Sprintf("%s-%s", config.RedirectCookieName, cookieId)
|
app.context.redirectCookieName = fmt.Sprintf("%s-%s", config.RedirectCookieName, cookieId)
|
||||||
|
|
||||||
// Dumps
|
// Dumps
|
||||||
log.Trace().Interface("config", app.config).Msg("Config dump")
|
tlog.App.Trace().Interface("config", app.config).Msg("Config dump")
|
||||||
log.Trace().Interface("users", app.context.users).Msg("Users dump")
|
tlog.App.Trace().Interface("users", app.context.users).Msg("Users dump")
|
||||||
log.Trace().Interface("oauthProviders", app.context.oauthProviders).Msg("OAuth providers dump")
|
tlog.App.Trace().Interface("oauthProviders", app.context.oauthProviders).Msg("OAuth providers dump")
|
||||||
log.Trace().Str("cookieDomain", app.context.cookieDomain).Msg("Cookie domain")
|
tlog.App.Trace().Str("cookieDomain", app.context.cookieDomain).Msg("Cookie domain")
|
||||||
log.Trace().Str("sessionCookieName", app.context.sessionCookieName).Msg("Session cookie name")
|
tlog.App.Trace().Str("sessionCookieName", app.context.sessionCookieName).Msg("Session cookie name")
|
||||||
log.Trace().Str("csrfCookieName", app.context.csrfCookieName).Msg("CSRF cookie name")
|
tlog.App.Trace().Str("csrfCookieName", app.context.csrfCookieName).Msg("CSRF cookie name")
|
||||||
log.Trace().Str("redirectCookieName", app.context.redirectCookieName).Msg("Redirect cookie name")
|
tlog.App.Trace().Str("redirectCookieName", app.context.redirectCookieName).Msg("Redirect cookie name")
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
db, err := app.SetupDatabase(app.config.DatabasePath)
|
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 {
|
if len(configuredProviders) == 0 {
|
||||||
return fmt.Errorf("no authentication providers configured")
|
return fmt.Errorf("no authentication providers configured")
|
||||||
@@ -169,28 +168,28 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start db cleanup routine
|
// Start db cleanup routine
|
||||||
log.Debug().Msg("Starting database cleanup routine")
|
tlog.App.Debug().Msg("Starting database cleanup routine")
|
||||||
go app.dbCleanup(queries)
|
go app.dbCleanup(queries)
|
||||||
|
|
||||||
// If analytics are not disabled, start heartbeat
|
// If analytics are not disabled, start heartbeat
|
||||||
if !app.config.DisableAnalytics {
|
if !app.config.DisableAnalytics {
|
||||||
log.Debug().Msg("Starting heartbeat routine")
|
tlog.App.Debug().Msg("Starting heartbeat routine")
|
||||||
go app.heartbeat()
|
go app.heartbeat()
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have an socket path, bind to it
|
// If we have an socket path, bind to it
|
||||||
if app.config.Server.SocketPath != "" {
|
if app.config.Server.SocketPath != "" {
|
||||||
if _, err := os.Stat(app.config.Server.SocketPath); err == nil {
|
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)
|
err := os.Remove(app.config.Server.SocketPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to remove existing socket file: %w", err)
|
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 {
|
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
|
return nil
|
||||||
@@ -198,9 +197,9 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
address := fmt.Sprintf("%s:%d", app.config.Server.Address, app.config.Server.Port)
|
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 {
|
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
|
return nil
|
||||||
@@ -223,7 +222,7 @@ func (app *BootstrapApp) heartbeat() {
|
|||||||
bodyJson, err := json.Marshal(body)
|
bodyJson, err := json.Marshal(body)
|
||||||
|
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,12 +233,12 @@ func (app *BootstrapApp) heartbeat() {
|
|||||||
heartbeatURL := config.ApiServer + "/v1/instances/heartbeat"
|
heartbeatURL := config.ApiServer + "/v1/instances/heartbeat"
|
||||||
|
|
||||||
for ; true; <-ticker.C {
|
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))
|
req, err := http.NewRequest(http.MethodPost, heartbeatURL, bytes.NewReader(bodyJson))
|
||||||
|
|
||||||
if err != nil {
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,14 +247,14 @@ func (app *BootstrapApp) heartbeat() {
|
|||||||
res, err := client.Do(req)
|
res, err := client.Do(req)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to send heartbeat")
|
tlog.App.Error().Err(err).Msg("Failed to send heartbeat")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
res.Body.Close()
|
res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != 200 && res.StatusCode != 201 {
|
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()
|
ctx := context.Background()
|
||||||
|
|
||||||
for ; true; <-ticker.C {
|
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())
|
err := queries.DeleteExpiredSessions(ctx, time.Now().Unix())
|
||||||
if err != nil {
|
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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ package bootstrap
|
|||||||
import (
|
import (
|
||||||
"github.com/steveiliop56/tinyauth/internal/repository"
|
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||||
"github.com/steveiliop56/tinyauth/internal/service"
|
"github.com/steveiliop56/tinyauth/internal/service"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Services struct {
|
type Services struct {
|
||||||
@@ -34,7 +33,7 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
services.ldapService = ldapService
|
services.ldapService = ldapService
|
||||||
} else {
|
} 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()
|
dockerService := service.NewDockerService()
|
||||||
|
|||||||
@@ -16,13 +16,11 @@ var RedirectCookieName = "tinyauth-redirect"
|
|||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
AppURL string `description:"The base URL where the app is hosted." yaml:"appUrl"`
|
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"`
|
ResourcesDir string `description:"The directory where resources are stored." yaml:"resourcesDir"`
|
||||||
DatabasePath string `description:"The path to the database file." yaml:"databasePath"`
|
DatabasePath string `description:"The path to the database file." yaml:"databasePath"`
|
||||||
DisableAnalytics bool `description:"Disable analytics." yaml:"disableAnalytics"`
|
DisableAnalytics bool `description:"Disable analytics." yaml:"disableAnalytics"`
|
||||||
DisableResources bool `description:"Disable resources server." yaml:"disableResources"`
|
DisableResources bool `description:"Disable resources server." yaml:"disableResources"`
|
||||||
DisableUIWarnings bool `description:"Disable UI warnings." yaml:"disableUIWarnings"`
|
DisableUIWarnings bool `description:"Disable UI warnings." yaml:"disableUIWarnings"`
|
||||||
LogJSON bool `description:"Enable JSON formatted logs." yaml:"logJSON"`
|
|
||||||
Server ServerConfig `description:"Server configuration." yaml:"server"`
|
Server ServerConfig `description:"Server configuration." yaml:"server"`
|
||||||
Auth AuthConfig `description:"Authentication configuration." yaml:"auth"`
|
Auth AuthConfig `description:"Authentication configuration." yaml:"auth"`
|
||||||
Apps map[string]App `description:"Application ACLs configuration." yaml:"apps"`
|
Apps map[string]App `description:"Application ACLs configuration." yaml:"apps"`
|
||||||
@@ -30,6 +28,7 @@ type Config struct {
|
|||||||
UI UIConfig `description:"UI customization." yaml:"ui"`
|
UI UIConfig `description:"UI customization." yaml:"ui"`
|
||||||
Ldap LdapConfig `description:"LDAP configuration." yaml:"ldap"`
|
Ldap LdapConfig `description:"LDAP configuration." yaml:"ldap"`
|
||||||
Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental"`
|
Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental"`
|
||||||
|
Log LogConfig `description:"Logging configuration." yaml:"log"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
@@ -78,6 +77,23 @@ type LdapConfig struct {
|
|||||||
AuthKey string `description:"Certificate key for mTLS authentication." yaml:"authKey"`
|
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 {
|
type ExperimentalConfig struct {
|
||||||
ConfigFile string `description:"Path to config file." yaml:"-"`
|
ConfigFile string `description:"Path to config file." yaml:"-"`
|
||||||
}
|
}
|
||||||
@@ -122,23 +138,16 @@ type User struct {
|
|||||||
TotpSecret string
|
TotpSecret string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LdapUser struct {
|
||||||
|
DN string
|
||||||
|
Groups []string
|
||||||
|
}
|
||||||
|
|
||||||
type UserSearch struct {
|
type UserSearch struct {
|
||||||
Username string
|
Username string
|
||||||
Type string // local, ldap or unknown
|
Type string // local, ldap or unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
type SessionCookie struct {
|
|
||||||
UUID string
|
|
||||||
Username string
|
|
||||||
Name string
|
|
||||||
Email string
|
|
||||||
Provider string
|
|
||||||
TotpPending bool
|
|
||||||
OAuthGroups string
|
|
||||||
OAuthName string
|
|
||||||
OAuthSub string
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserContext struct {
|
type UserContext struct {
|
||||||
Username string
|
Username string
|
||||||
Name string
|
Name string
|
||||||
@@ -151,6 +160,7 @@ type UserContext struct {
|
|||||||
TotpEnabled bool
|
TotpEnabled bool
|
||||||
OAuthName string
|
OAuthName string
|
||||||
OAuthSub string
|
OAuthSub string
|
||||||
|
LdapGroups string
|
||||||
}
|
}
|
||||||
|
|
||||||
// API responses and queries
|
// API responses and queries
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserContextResponse struct {
|
type UserContextResponse struct {
|
||||||
@@ -61,7 +61,7 @@ type ContextController struct {
|
|||||||
|
|
||||||
func NewContextController(config ContextControllerConfig, router *gin.RouterGroup) *ContextController {
|
func NewContextController(config ContextControllerConfig, router *gin.RouterGroup) *ContextController {
|
||||||
if config.DisableUIWarnings {
|
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{
|
return &ContextController{
|
||||||
@@ -94,7 +94,7 @@ func (controller *ContextController) userContextHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
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.Status = 401
|
||||||
userContext.Message = "Unauthorized"
|
userContext.Message = "Unauthorized"
|
||||||
userContext.IsLoggedIn = false
|
userContext.IsLoggedIn = false
|
||||||
@@ -108,7 +108,7 @@ func (controller *ContextController) userContextHandler(c *gin.Context) {
|
|||||||
func (controller *ContextController) appContextHandler(c *gin.Context) {
|
func (controller *ContextController) appContextHandler(c *gin.Context) {
|
||||||
appUrl, err := url.Parse(controller.config.AppURL)
|
appUrl, err := url.Parse(controller.config.AppURL)
|
||||||
if err != nil {
|
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{
|
c.JSON(500, gin.H{
|
||||||
"status": 500,
|
"status": 500,
|
||||||
"message": "Internal Server Error",
|
"message": "Internal Server Error",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
@@ -48,6 +49,8 @@ var userContext = config.UserContext{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func setupContextController(middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder) {
|
func setupContextController(middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder) {
|
||||||
|
tlog.NewSimpleLogger().Init()
|
||||||
|
|
||||||
// Setup
|
// Setup
|
||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
router := gin.Default()
|
router := gin.Default()
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||||
"github.com/steveiliop56/tinyauth/internal/service"
|
"github.com/steveiliop56/tinyauth/internal/service"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/go-querystring/query"
|
"github.com/google/go-querystring/query"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type OAuthRequest struct {
|
type OAuthRequest struct {
|
||||||
@@ -54,7 +55,7 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
|
|||||||
|
|
||||||
err := c.BindUri(&req)
|
err := c.BindUri(&req)
|
||||||
if err != nil {
|
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{
|
c.JSON(400, gin.H{
|
||||||
"status": 400,
|
"status": 400,
|
||||||
"message": "Bad Request",
|
"message": "Bad Request",
|
||||||
@@ -65,7 +66,7 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
|
|||||||
service, exists := controller.broker.GetService(req.Provider)
|
service, exists := controller.broker.GetService(req.Provider)
|
||||||
|
|
||||||
if !exists {
|
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{
|
c.JSON(404, gin.H{
|
||||||
"status": 404,
|
"status": 404,
|
||||||
"message": "Not Found",
|
"message": "Not Found",
|
||||||
@@ -82,12 +83,12 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
|
|||||||
isRedirectSafe := utils.IsRedirectSafe(redirectURI, controller.config.CookieDomain)
|
isRedirectSafe := utils.IsRedirectSafe(redirectURI, controller.config.CookieDomain)
|
||||||
|
|
||||||
if !isRedirectSafe {
|
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 = ""
|
redirectURI = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
if redirectURI != "" && isRedirectSafe {
|
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)
|
c.SetCookie(controller.config.RedirectCookieName, redirectURI, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +104,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
|
|
||||||
err := c.BindUri(&req)
|
err := c.BindUri(&req)
|
||||||
if err != nil {
|
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{
|
c.JSON(400, gin.H{
|
||||||
"status": 400,
|
"status": 400,
|
||||||
"message": "Bad Request",
|
"message": "Bad Request",
|
||||||
@@ -115,7 +116,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
csrfCookie, err := c.Cookie(controller.config.CSRFCookieName)
|
csrfCookie, err := c.Cookie(controller.config.CSRFCookieName)
|
||||||
|
|
||||||
if err != nil || state != csrfCookie {
|
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.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))
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||||
return
|
return
|
||||||
@@ -127,14 +128,14 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
service, exists := controller.broker.GetService(req.Provider)
|
service, exists := controller.broker.GetService(req.Provider)
|
||||||
|
|
||||||
if !exists {
|
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))
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = service.VerifyCode(code)
|
err = service.VerifyCode(code)
|
||||||
if err != nil {
|
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))
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -142,26 +143,27 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
user, err := controller.broker.GetUser(req.Provider)
|
user, err := controller.broker.GetUser(req.Provider)
|
||||||
|
|
||||||
if err != nil {
|
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))
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.Email == "" {
|
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))
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !controller.auth.IsEmailWhitelisted(user.Email) {
|
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{
|
queries, err := query.Values(config.UnauthorizedQuery{
|
||||||
Username: user.Email,
|
Username: user.Email,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
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))
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -173,24 +175,24 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
var name string
|
var name string
|
||||||
|
|
||||||
if strings.TrimSpace(user.Name) != "" {
|
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
|
name = user.Name
|
||||||
} else {
|
} 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])
|
name = fmt.Sprintf("%s (%s)", utils.Capitalize(strings.Split(user.Email, "@")[0]), strings.Split(user.Email, "@")[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
var username string
|
var username string
|
||||||
|
|
||||||
if strings.TrimSpace(user.PreferredUsername) != "" {
|
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
|
username = user.PreferredUsername
|
||||||
} else {
|
} 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)
|
username = strings.Replace(user.Email, "@", "_", -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionCookie := config.SessionCookie{
|
sessionCookie := repository.Session{
|
||||||
Username: username,
|
Username: username,
|
||||||
Name: name,
|
Name: name,
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
@@ -200,20 +202,22 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
OAuthSub: user.Sub,
|
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)
|
err = controller.auth.CreateSessionCookie(c, &sessionCookie)
|
||||||
|
|
||||||
if err != nil {
|
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))
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tlog.AuditLoginSuccess(c, sessionCookie.Username, sessionCookie.Provider)
|
||||||
|
|
||||||
redirectURI, err := c.Cookie(controller.config.RedirectCookieName)
|
redirectURI, err := c.Cookie(controller.config.RedirectCookieName)
|
||||||
|
|
||||||
if err != nil || !utils.IsRedirectSafe(redirectURI, controller.config.CookieDomain) {
|
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)
|
c.Redirect(http.StatusTemporaryRedirect, controller.config.AppURL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -223,7 +227,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
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))
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import (
|
|||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
"github.com/steveiliop56/tinyauth/internal/service"
|
"github.com/steveiliop56/tinyauth/internal/service"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/go-querystring/query"
|
"github.com/google/go-querystring/query"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var SupportedProxies = []string{"nginx", "traefik", "caddy", "envoy"}
|
var SupportedProxies = []string{"nginx", "traefik", "caddy", "envoy"}
|
||||||
@@ -52,7 +52,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
|
|
||||||
err := c.BindUri(&req)
|
err := c.BindUri(&req)
|
||||||
if err != nil {
|
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{
|
c.JSON(400, gin.H{
|
||||||
"status": 400,
|
"status": 400,
|
||||||
"message": "Bad Request",
|
"message": "Bad Request",
|
||||||
@@ -61,7 +61,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !slices.Contains(SupportedProxies, req.Proxy) {
|
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{
|
c.JSON(400, gin.H{
|
||||||
"status": 400,
|
"status": 400,
|
||||||
"message": "Bad Request",
|
"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
|
// Envoy uses the original client method for the external auth request
|
||||||
// so we allow Any standard HTTP method for /api/auth/envoy
|
// so we allow Any standard HTTP method for /api/auth/envoy
|
||||||
if req.Proxy != "envoy" && c.Request.Method != http.MethodGet {
|
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.Header("Allow", "GET")
|
||||||
c.JSON(405, gin.H{
|
c.JSON(405, gin.H{
|
||||||
"status": 405,
|
"status": 405,
|
||||||
@@ -85,9 +85,9 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
isBrowser := strings.Contains(c.Request.Header.Get("Accept"), "text/html")
|
isBrowser := strings.Contains(c.Request.Header.Get("Accept"), "text/html")
|
||||||
|
|
||||||
if isBrowser {
|
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 {
|
} 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")
|
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)
|
acls, err := controller.acls.GetAccessControls(host)
|
||||||
|
|
||||||
if err != nil {
|
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)
|
controller.handleError(c, req, isBrowser)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Trace().Interface("acls", acls).Msg("ACLs for resource")
|
tlog.App.Trace().Interface("acls", acls).Msg("ACLs for resource")
|
||||||
|
|
||||||
clientIP := c.ClientIP()
|
clientIP := c.ClientIP()
|
||||||
|
|
||||||
@@ -119,13 +119,13 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
authEnabled, err := controller.auth.IsAuthEnabled(uri, acls.Path)
|
authEnabled, err := controller.auth.IsAuthEnabled(uri, acls.Path)
|
||||||
|
|
||||||
if err != nil {
|
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)
|
controller.handleError(c, req, isBrowser)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !authEnabled {
|
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)
|
controller.setHeaders(c, acls)
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
@@ -149,7 +149,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
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))
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -163,7 +163,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
context, err := utils.GetContext(c)
|
context, err := utils.GetContext(c)
|
||||||
|
|
||||||
if err != nil {
|
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{
|
userContext = config.UserContext{
|
||||||
IsLoggedIn: false,
|
IsLoggedIn: false,
|
||||||
}
|
}
|
||||||
@@ -171,10 +171,10 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
userContext = 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 {
|
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
|
userContext.IsLoggedIn = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,7 +182,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
userAllowed := controller.auth.IsUserAllowed(c, userContext, acls)
|
userAllowed := controller.auth.IsUserAllowed(c, userContext, acls)
|
||||||
|
|
||||||
if !userAllowed {
|
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 {
|
if req.Proxy == "nginx" || !isBrowser {
|
||||||
c.JSON(403, gin.H{
|
c.JSON(403, gin.H{
|
||||||
@@ -197,7 +197,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
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))
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -216,7 +216,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
groupOK := controller.auth.IsInOAuthGroup(c, userContext, acls.OAuth.Groups)
|
groupOK := controller.auth.IsInOAuthGroup(c, userContext, acls.OAuth.Groups)
|
||||||
|
|
||||||
if !groupOK {
|
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 {
|
if req.Proxy == "nginx" || !isBrowser {
|
||||||
c.JSON(403, gin.H{
|
c.JSON(403, gin.H{
|
||||||
@@ -232,7 +232,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
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))
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -276,7 +276,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
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))
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -290,14 +290,14 @@ func (controller *ProxyController) setHeaders(c *gin.Context, acls config.App) {
|
|||||||
headers := utils.ParseHeaders(acls.Response.Headers)
|
headers := utils.ParseHeaders(acls.Response.Headers)
|
||||||
|
|
||||||
for key, value := range 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)
|
c.Header(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
basicPassword := utils.GetSecret(acls.Response.BasicAuth.Password, acls.Response.BasicAuth.PasswordFile)
|
basicPassword := utils.GetSecret(acls.Response.BasicAuth.Password, acls.Response.BasicAuth.PasswordFile)
|
||||||
|
|
||||||
if acls.Response.BasicAuth.Username != "" && basicPassword != "" {
|
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)))
|
c.Header("Authorization", fmt.Sprintf("Basic %s", utils.GetBasicAuth(acls.Response.BasicAuth.Username, basicPassword)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,15 @@ import (
|
|||||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||||
"github.com/steveiliop56/tinyauth/internal/repository"
|
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||||
"github.com/steveiliop56/tinyauth/internal/service"
|
"github.com/steveiliop56/tinyauth/internal/service"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func setupProxyController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder, *service.AuthService) {
|
func setupProxyController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder, *service.AuthService) {
|
||||||
|
tlog.NewSimpleLogger().Init()
|
||||||
|
|
||||||
// Setup
|
// Setup
|
||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
router := gin.Default()
|
router := gin.Default()
|
||||||
@@ -140,7 +143,7 @@ func TestProxyHandler(t *testing.T) {
|
|||||||
// Test logged in user
|
// Test logged in user
|
||||||
c := gin.CreateTestContextOnly(recorder, router)
|
c := gin.CreateTestContextOnly(recorder, router)
|
||||||
|
|
||||||
err := authService.CreateSessionCookie(c, &config.SessionCookie{
|
err := authService.CreateSessionCookie(c, &repository.Session{
|
||||||
Username: "testuser",
|
Username: "testuser",
|
||||||
Name: "testuser",
|
Name: "testuser",
|
||||||
Email: "testuser@example.com",
|
Email: "testuser@example.com",
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||||
"github.com/steveiliop56/tinyauth/internal/service"
|
"github.com/steveiliop56/tinyauth/internal/service"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/pquerna/otp/totp"
|
"github.com/pquerna/otp/totp"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type LoginRequest struct {
|
type LoginRequest struct {
|
||||||
@@ -53,7 +53,7 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
|
|
||||||
err := c.ShouldBindJSON(&req)
|
err := c.ShouldBindJSON(&req)
|
||||||
if err != nil {
|
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{
|
c.JSON(400, gin.H{
|
||||||
"status": 400,
|
"status": 400,
|
||||||
"message": "Bad Request",
|
"message": "Bad Request",
|
||||||
@@ -61,12 +61,13 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
return
|
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)
|
isLocked, remaining := controller.auth.IsAccountLocked(req.Username)
|
||||||
|
|
||||||
if isLocked {
|
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-locked", "true")
|
||||||
c.Writer.Header().Add("x-tinyauth-lock-reset", time.Now().Add(time.Duration(remaining)*time.Second).Format(time.RFC3339))
|
c.Writer.Header().Add("x-tinyauth-lock-reset", time.Now().Add(time.Duration(remaining)*time.Second).Format(time.RFC3339))
|
||||||
c.JSON(429, gin.H{
|
c.JSON(429, gin.H{
|
||||||
@@ -79,8 +80,9 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
userSearch := controller.auth.SearchUser(req.Username)
|
userSearch := controller.auth.SearchUser(req.Username)
|
||||||
|
|
||||||
if userSearch.Type == "unknown" {
|
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)
|
controller.auth.RecordLoginAttempt(req.Username, false)
|
||||||
|
tlog.AuditLoginFailure(c, req.Username, "username", "user not found")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, gin.H{
|
||||||
"status": 401,
|
"status": 401,
|
||||||
"message": "Unauthorized",
|
"message": "Unauthorized",
|
||||||
@@ -89,8 +91,9 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !controller.auth.VerifyUser(userSearch, req.Password) {
|
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)
|
controller.auth.RecordLoginAttempt(req.Username, false)
|
||||||
|
tlog.AuditLoginFailure(c, req.Username, "username", "invalid password")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, gin.H{
|
||||||
"status": 401,
|
"status": 401,
|
||||||
"message": "Unauthorized",
|
"message": "Unauthorized",
|
||||||
@@ -98,7 +101,8 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
return
|
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)
|
controller.auth.RecordLoginAttempt(req.Username, true)
|
||||||
|
|
||||||
@@ -106,9 +110,9 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
user := controller.auth.GetLocalUser(userSearch.Username)
|
user := controller.auth.GetLocalUser(userSearch.Username)
|
||||||
|
|
||||||
if user.TotpSecret != "" {
|
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, &config.SessionCookie{
|
err := controller.auth.CreateSessionCookie(c, &repository.Session{
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Name: utils.Capitalize(req.Username),
|
Name: utils.Capitalize(req.Username),
|
||||||
Email: fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.config.CookieDomain),
|
Email: fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.config.CookieDomain),
|
||||||
@@ -117,7 +121,7 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
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{
|
c.JSON(500, gin.H{
|
||||||
"status": 500,
|
"status": 500,
|
||||||
"message": "Internal Server Error",
|
"message": "Internal Server Error",
|
||||||
@@ -134,19 +138,34 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionCookie := config.SessionCookie{
|
sessionCookie := repository.Session{
|
||||||
Username: req.Username,
|
Username: req.Username,
|
||||||
Name: utils.Capitalize(req.Username),
|
Name: utils.Capitalize(req.Username),
|
||||||
Email: fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.config.CookieDomain),
|
Email: fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.config.CookieDomain),
|
||||||
Provider: "username",
|
Provider: "username",
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
if userSearch.Type == "ldap" {
|
||||||
|
ldapUser, err := controller.auth.GetLdapUser(userSearch.Username)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
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",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionCookie.LdapGroups = strings.Join(ldapUser.Groups, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
tlog.App.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
|
||||||
|
|
||||||
err = controller.auth.CreateSessionCookie(c, &sessionCookie)
|
err = controller.auth.CreateSessionCookie(c, &sessionCookie)
|
||||||
|
|
||||||
if err != nil {
|
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{
|
c.JSON(500, gin.H{
|
||||||
"status": 500,
|
"status": 500,
|
||||||
"message": "Internal Server Error",
|
"message": "Internal Server Error",
|
||||||
@@ -161,10 +180,15 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (controller *UserController) logoutHandler(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)
|
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{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
"message": "Logout successful",
|
"message": "Logout successful",
|
||||||
@@ -176,7 +200,7 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
|
|
||||||
err := c.ShouldBindJSON(&req)
|
err := c.ShouldBindJSON(&req)
|
||||||
if err != nil {
|
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{
|
c.JSON(400, gin.H{
|
||||||
"status": 400,
|
"status": 400,
|
||||||
"message": "Bad Request",
|
"message": "Bad Request",
|
||||||
@@ -187,7 +211,7 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
context, err := utils.GetContext(c)
|
context, err := utils.GetContext(c)
|
||||||
|
|
||||||
if err != nil {
|
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{
|
c.JSON(500, gin.H{
|
||||||
"status": 500,
|
"status": 500,
|
||||||
"message": "Internal Server Error",
|
"message": "Internal Server Error",
|
||||||
@@ -196,7 +220,7 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !context.TotpPending {
|
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{
|
c.JSON(401, gin.H{
|
||||||
"status": 401,
|
"status": 401,
|
||||||
"message": "Unauthorized",
|
"message": "Unauthorized",
|
||||||
@@ -204,12 +228,12 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
return
|
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)
|
isLocked, remaining := controller.auth.IsAccountLocked(context.Username)
|
||||||
|
|
||||||
if isLocked {
|
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-locked", "true")
|
||||||
c.Writer.Header().Add("x-tinyauth-lock-reset", time.Now().Add(time.Duration(remaining)*time.Second).Format(time.RFC3339))
|
c.Writer.Header().Add("x-tinyauth-lock-reset", time.Now().Add(time.Duration(remaining)*time.Second).Format(time.RFC3339))
|
||||||
c.JSON(429, gin.H{
|
c.JSON(429, gin.H{
|
||||||
@@ -224,8 +248,9 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
ok := totp.Validate(req.Code, user.TotpSecret)
|
ok := totp.Validate(req.Code, user.TotpSecret)
|
||||||
|
|
||||||
if !ok {
|
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)
|
controller.auth.RecordLoginAttempt(context.Username, false)
|
||||||
|
tlog.AuditLoginFailure(c, context.Username, "totp", "invalid totp code")
|
||||||
c.JSON(401, gin.H{
|
c.JSON(401, gin.H{
|
||||||
"status": 401,
|
"status": 401,
|
||||||
"message": "Unauthorized",
|
"message": "Unauthorized",
|
||||||
@@ -233,23 +258,24 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
|||||||
return
|
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)
|
controller.auth.RecordLoginAttempt(context.Username, true)
|
||||||
|
|
||||||
sessionCookie := config.SessionCookie{
|
sessionCookie := repository.Session{
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Name: utils.Capitalize(user.Username),
|
Name: utils.Capitalize(user.Username),
|
||||||
Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), controller.config.CookieDomain),
|
Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), controller.config.CookieDomain),
|
||||||
Provider: "username",
|
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)
|
err = controller.auth.CreateSessionCookie(c, &sessionCookie)
|
||||||
|
|
||||||
if err != nil {
|
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{
|
c.JSON(500, gin.H{
|
||||||
"status": 500,
|
"status": 500,
|
||||||
"message": "Internal Server Error",
|
"message": "Internal Server Error",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||||
"github.com/steveiliop56/tinyauth/internal/repository"
|
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||||
"github.com/steveiliop56/tinyauth/internal/service"
|
"github.com/steveiliop56/tinyauth/internal/service"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/pquerna/otp/totp"
|
"github.com/pquerna/otp/totp"
|
||||||
@@ -23,6 +24,8 @@ var cookieValue string
|
|||||||
var totpSecret = "6WFZXPEZRK5MZHHYAFW4DAOUYQMCASBJ"
|
var totpSecret = "6WFZXPEZRK5MZHHYAFW4DAOUYQMCASBJ"
|
||||||
|
|
||||||
func setupUserController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder) {
|
func setupUserController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder) {
|
||||||
|
tlog.NewSimpleLogger().Init()
|
||||||
|
|
||||||
// Setup
|
// Setup
|
||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
router := gin.Default()
|
router := gin.Default()
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import (
|
|||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
"github.com/steveiliop56/tinyauth/internal/service"
|
"github.com/steveiliop56/tinyauth/internal/service"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ContextMiddlewareConfig struct {
|
type ContextMiddlewareConfig struct {
|
||||||
@@ -40,7 +40,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
cookie, err := m.auth.GetSessionCookie(c)
|
cookie, err := m.auth.GetSessionCookie(c)
|
||||||
|
|
||||||
if err != nil {
|
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
|
goto basic
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
userSearch := m.auth.SearchUser(cookie.Username)
|
userSearch := m.auth.SearchUser(cookie.Username)
|
||||||
|
|
||||||
if userSearch.Type == "unknown" || userSearch.Type == "error" {
|
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)
|
m.auth.DeleteSessionCookie(c)
|
||||||
goto basic
|
goto basic
|
||||||
}
|
}
|
||||||
@@ -74,6 +74,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
Email: cookie.Email,
|
Email: cookie.Email,
|
||||||
Provider: "username",
|
Provider: "username",
|
||||||
IsLoggedIn: true,
|
IsLoggedIn: true,
|
||||||
|
LdapGroups: cookie.LdapGroups,
|
||||||
})
|
})
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
@@ -81,13 +82,13 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
_, exists := m.broker.GetService(cookie.Provider)
|
_, exists := m.broker.GetService(cookie.Provider)
|
||||||
|
|
||||||
if !exists {
|
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)
|
m.auth.DeleteSessionCookie(c)
|
||||||
goto basic
|
goto basic
|
||||||
}
|
}
|
||||||
|
|
||||||
if !m.auth.IsEmailWhitelisted(cookie.Email) {
|
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)
|
m.auth.DeleteSessionCookie(c)
|
||||||
goto basic
|
goto basic
|
||||||
}
|
}
|
||||||
@@ -112,7 +113,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
basic := m.auth.GetBasicAuth(c)
|
basic := m.auth.GetBasicAuth(c)
|
||||||
|
|
||||||
if basic == nil {
|
if basic == nil {
|
||||||
log.Debug().Msg("No basic auth provided")
|
tlog.App.Debug().Msg("No basic auth provided")
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -120,7 +121,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
locked, remaining := m.auth.IsAccountLocked(basic.Username)
|
locked, remaining := m.auth.IsAccountLocked(basic.Username)
|
||||||
|
|
||||||
if locked {
|
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-locked", "true")
|
||||||
c.Writer.Header().Add("x-tinyauth-lock-reset", time.Now().Add(time.Duration(remaining)*time.Second).Format(time.RFC3339))
|
c.Writer.Header().Add("x-tinyauth-lock-reset", time.Now().Add(time.Duration(remaining)*time.Second).Format(time.RFC3339))
|
||||||
c.Next()
|
c.Next()
|
||||||
@@ -131,14 +132,14 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
|
|
||||||
if userSearch.Type == "unknown" || userSearch.Type == "error" {
|
if userSearch.Type == "unknown" || userSearch.Type == "error" {
|
||||||
m.auth.RecordLoginAttempt(basic.Username, false)
|
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()
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !m.auth.VerifyUser(userSearch, basic.Password) {
|
if !m.auth.VerifyUser(userSearch, basic.Password) {
|
||||||
m.auth.RecordLoginAttempt(basic.Username, false)
|
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()
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -147,7 +148,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
|
|
||||||
switch userSearch.Type {
|
switch userSearch.Type {
|
||||||
case "local":
|
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)
|
user := m.auth.GetLocalUser(basic.Username)
|
||||||
|
|
||||||
@@ -155,20 +156,30 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Name: utils.Capitalize(user.Username),
|
Name: utils.Capitalize(user.Username),
|
||||||
Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), m.config.CookieDomain),
|
Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), m.config.CookieDomain),
|
||||||
Provider: "basic",
|
Provider: "username",
|
||||||
IsLoggedIn: true,
|
IsLoggedIn: true,
|
||||||
TotpEnabled: user.TotpSecret != "",
|
TotpEnabled: user.TotpSecret != "",
|
||||||
})
|
})
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
case "ldap":
|
case "ldap":
|
||||||
log.Debug().Msg("Basic auth user is LDAP")
|
tlog.App.Debug().Msg("Basic auth user is LDAP")
|
||||||
|
|
||||||
|
ldapUser, err := m.auth.GetLdapUser(basic.Username)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
tlog.App.Debug().Err(err).Msg("Error retrieving LDAP user details")
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
c.Set("context", &config.UserContext{
|
c.Set("context", &config.UserContext{
|
||||||
Username: basic.Username,
|
Username: basic.Username,
|
||||||
Name: utils.Capitalize(basic.Username),
|
Name: utils.Capitalize(basic.Username),
|
||||||
Email: fmt.Sprintf("%s@%s", strings.ToLower(basic.Username), m.config.CookieDomain),
|
Email: fmt.Sprintf("%s@%s", strings.ToLower(basic.Username), m.config.CookieDomain),
|
||||||
Provider: "basic",
|
Provider: "ldap",
|
||||||
IsLoggedIn: true,
|
IsLoggedIn: true,
|
||||||
|
LdapGroups: strings.Join(ldapUser.Groups, ","),
|
||||||
})
|
})
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -49,7 +49,7 @@ func (m *ZerologMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
|
|
||||||
latency := time.Since(tStart).String()
|
latency := time.Since(tStart).String()
|
||||||
|
|
||||||
subLogger := log.With().Str("method", method).
|
subLogger := tlog.HTTP.With().Str("method", method).
|
||||||
Str("path", path).
|
Str("path", path).
|
||||||
Str("address", address).
|
Str("address", address).
|
||||||
Str("client_ip", clientIP).
|
Str("client_ip", clientIP).
|
||||||
|
|||||||
@@ -16,4 +16,5 @@ type Session struct {
|
|||||||
CreatedAt int64
|
CreatedAt int64
|
||||||
OAuthName string
|
OAuthName string
|
||||||
OAuthSub string
|
OAuthSub string
|
||||||
|
LdapGroups string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,11 +21,12 @@ INSERT INTO sessions (
|
|||||||
"expiry",
|
"expiry",
|
||||||
"created_at",
|
"created_at",
|
||||||
"oauth_name",
|
"oauth_name",
|
||||||
"oauth_sub"
|
"oauth_sub",
|
||||||
|
"ldap_groups"
|
||||||
) VALUES (
|
) VALUES (
|
||||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||||
)
|
)
|
||||||
RETURNING uuid, username, email, name, provider, totp_pending, oauth_groups, expiry, created_at, oauth_name, oauth_sub
|
RETURNING uuid, username, email, name, provider, totp_pending, oauth_groups, expiry, created_at, oauth_name, oauth_sub, ldap_groups
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateSessionParams struct {
|
type CreateSessionParams struct {
|
||||||
@@ -40,6 +41,7 @@ type CreateSessionParams struct {
|
|||||||
CreatedAt int64
|
CreatedAt int64
|
||||||
OAuthName string
|
OAuthName string
|
||||||
OAuthSub string
|
OAuthSub string
|
||||||
|
LdapGroups string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error) {
|
func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error) {
|
||||||
@@ -55,6 +57,7 @@ func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (S
|
|||||||
arg.CreatedAt,
|
arg.CreatedAt,
|
||||||
arg.OAuthName,
|
arg.OAuthName,
|
||||||
arg.OAuthSub,
|
arg.OAuthSub,
|
||||||
|
arg.LdapGroups,
|
||||||
)
|
)
|
||||||
var i Session
|
var i Session
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
@@ -69,6 +72,7 @@ func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (S
|
|||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.OAuthName,
|
&i.OAuthName,
|
||||||
&i.OAuthSub,
|
&i.OAuthSub,
|
||||||
|
&i.LdapGroups,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@@ -94,7 +98,7 @@ func (q *Queries) DeleteSession(ctx context.Context, uuid string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getSession = `-- name: GetSession :one
|
const getSession = `-- name: GetSession :one
|
||||||
SELECT uuid, username, email, name, provider, totp_pending, oauth_groups, expiry, created_at, oauth_name, oauth_sub FROM "sessions"
|
SELECT uuid, username, email, name, provider, totp_pending, oauth_groups, expiry, created_at, oauth_name, oauth_sub, ldap_groups FROM "sessions"
|
||||||
WHERE "uuid" = ?
|
WHERE "uuid" = ?
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -113,6 +117,7 @@ func (q *Queries) GetSession(ctx context.Context, uuid string) (Session, error)
|
|||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.OAuthName,
|
&i.OAuthName,
|
||||||
&i.OAuthSub,
|
&i.OAuthSub,
|
||||||
|
&i.LdapGroups,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@@ -127,9 +132,10 @@ UPDATE "sessions" SET
|
|||||||
"oauth_groups" = ?,
|
"oauth_groups" = ?,
|
||||||
"expiry" = ?,
|
"expiry" = ?,
|
||||||
"oauth_name" = ?,
|
"oauth_name" = ?,
|
||||||
"oauth_sub" = ?
|
"oauth_sub" = ?,
|
||||||
|
"ldap_groups" = ?
|
||||||
WHERE "uuid" = ?
|
WHERE "uuid" = ?
|
||||||
RETURNING uuid, username, email, name, provider, totp_pending, oauth_groups, expiry, created_at, oauth_name, oauth_sub
|
RETURNING uuid, username, email, name, provider, totp_pending, oauth_groups, expiry, created_at, oauth_name, oauth_sub, ldap_groups
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateSessionParams struct {
|
type UpdateSessionParams struct {
|
||||||
@@ -142,6 +148,7 @@ type UpdateSessionParams struct {
|
|||||||
Expiry int64
|
Expiry int64
|
||||||
OAuthName string
|
OAuthName string
|
||||||
OAuthSub string
|
OAuthSub string
|
||||||
|
LdapGroups string
|
||||||
UUID string
|
UUID string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,6 +163,7 @@ func (q *Queries) UpdateSession(ctx context.Context, arg UpdateSessionParams) (S
|
|||||||
arg.Expiry,
|
arg.Expiry,
|
||||||
arg.OAuthName,
|
arg.OAuthName,
|
||||||
arg.OAuthSub,
|
arg.OAuthSub,
|
||||||
|
arg.LdapGroups,
|
||||||
arg.UUID,
|
arg.UUID,
|
||||||
)
|
)
|
||||||
var i Session
|
var i Session
|
||||||
@@ -171,6 +179,7 @@ func (q *Queries) UpdateSession(ctx context.Context, arg UpdateSessionParams) (S
|
|||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.OAuthName,
|
&i.OAuthName,
|
||||||
&i.OAuthSub,
|
&i.OAuthSub,
|
||||||
|
&i.LdapGroups,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AccessControlsService struct {
|
type AccessControlsService struct {
|
||||||
@@ -27,12 +27,12 @@ func (acls *AccessControlsService) Init() error {
|
|||||||
func (acls *AccessControlsService) lookupStaticACLs(domain string) (config.App, error) {
|
func (acls *AccessControlsService) lookupStaticACLs(domain string) (config.App, error) {
|
||||||
for app, config := range acls.static {
|
for app, config := range acls.static {
|
||||||
if config.Config.Domain == domain {
|
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
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.SplitN(domain, ".", 2)[0] == app {
|
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
|
return config, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -44,11 +44,11 @@ func (acls *AccessControlsService) GetAccessControls(domain string) (config.App,
|
|||||||
app, err := acls.lookupStaticACLs(domain)
|
app, err := acls.lookupStaticACLs(domain)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
log.Debug().Msg("Using ACls from static configuration")
|
tlog.App.Debug().Msg("Using ACls from static configuration")
|
||||||
return app, nil
|
return app, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to Docker labels
|
// 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)
|
return acls.docker.GetLabels(domain)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ import (
|
|||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
"github.com/steveiliop56/tinyauth/internal/repository"
|
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -70,10 +70,10 @@ func (auth *AuthService) SearchUser(username string) config.UserSearch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if auth.ldap != nil {
|
if auth.ldap != nil {
|
||||||
userDN, err := auth.ldap.Search(username)
|
userDN, err := auth.ldap.GetUserDN(username)
|
||||||
|
|
||||||
if err != nil {
|
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{
|
return config.UserSearch{
|
||||||
Type: "error",
|
Type: "error",
|
||||||
}
|
}
|
||||||
@@ -99,24 +99,24 @@ func (auth *AuthService) VerifyUser(search config.UserSearch, password string) b
|
|||||||
if auth.ldap != nil {
|
if auth.ldap != nil {
|
||||||
err := auth.ldap.Bind(search.Username, password)
|
err := auth.ldap.Bind(search.Username, password)
|
||||||
if err != nil {
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
err = auth.ldap.BindService(true)
|
err = auth.ldap.BindService(true)
|
||||||
if err != nil {
|
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 false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
default:
|
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
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,10 +127,23 @@ 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{}
|
return config.User{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (auth *AuthService) GetLdapUser(userDN string) (config.LdapUser, error) {
|
||||||
|
groups, err := auth.ldap.GetUserGroups(userDN)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return config.LdapUser{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.LdapUser{
|
||||||
|
DN: userDN,
|
||||||
|
Groups: groups,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (auth *AuthService) CheckPassword(user config.User, password string) bool {
|
func (auth *AuthService) CheckPassword(user config.User, password string) bool {
|
||||||
return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil
|
return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil
|
||||||
}
|
}
|
||||||
@@ -182,7 +195,7 @@ func (auth *AuthService) RecordLoginAttempt(identifier string, success bool) {
|
|||||||
|
|
||||||
if attempt.FailedAttempts >= auth.config.LoginMaxRetries {
|
if attempt.FailedAttempts >= auth.config.LoginMaxRetries {
|
||||||
attempt.LockedUntil = time.Now().Add(time.Duration(auth.config.LoginTimeout) * time.Second)
|
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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +203,7 @@ func (auth *AuthService) IsEmailWhitelisted(email string) bool {
|
|||||||
return utils.CheckFilter(strings.Join(auth.config.OauthWhitelist, ","), email)
|
return utils.CheckFilter(strings.Join(auth.config.OauthWhitelist, ","), email)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *config.SessionCookie) error {
|
func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *repository.Session) error {
|
||||||
uuid, err := uuid.NewRandom()
|
uuid, err := uuid.NewRandom()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -217,6 +230,7 @@ func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *config.Sessio
|
|||||||
CreatedAt: time.Now().Unix(),
|
CreatedAt: time.Now().Unix(),
|
||||||
OAuthName: data.OAuthName,
|
OAuthName: data.OAuthName,
|
||||||
OAuthSub: data.OAuthSub,
|
OAuthSub: data.OAuthSub,
|
||||||
|
LdapGroups: data.LdapGroups,
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = auth.queries.CreateSession(c, session)
|
_, err = auth.queries.CreateSession(c, session)
|
||||||
@@ -270,6 +284,7 @@ func (auth *AuthService) RefreshSessionCookie(c *gin.Context) error {
|
|||||||
OAuthName: session.OAuthName,
|
OAuthName: session.OAuthName,
|
||||||
OAuthSub: session.OAuthSub,
|
OAuthSub: session.OAuthSub,
|
||||||
UUID: session.UUID,
|
UUID: session.UUID,
|
||||||
|
LdapGroups: session.LdapGroups,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -277,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)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
@@ -300,20 +315,20 @@ func (auth *AuthService) DeleteSessionCookie(c *gin.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie, error) {
|
func (auth *AuthService) GetSessionCookie(c *gin.Context) (repository.Session, error) {
|
||||||
cookie, err := c.Cookie(auth.config.SessionCookieName)
|
cookie, err := c.Cookie(auth.config.SessionCookieName)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return config.SessionCookie{}, err
|
return repository.Session{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
session, err := auth.queries.GetSession(c, cookie)
|
session, err := auth.queries.GetSession(c, cookie)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return config.SessionCookie{}, fmt.Errorf("session not found")
|
return repository.Session{}, fmt.Errorf("session not found")
|
||||||
}
|
}
|
||||||
return config.SessionCookie{}, err
|
return repository.Session{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
currentTime := time.Now().Unix()
|
currentTime := time.Now().Unix()
|
||||||
@@ -322,21 +337,21 @@ func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie,
|
|||||||
if currentTime-session.CreatedAt > int64(auth.config.SessionMaxLifetime) {
|
if currentTime-session.CreatedAt > int64(auth.config.SessionMaxLifetime) {
|
||||||
err = auth.queries.DeleteSession(c, cookie)
|
err = auth.queries.DeleteSession(c, cookie)
|
||||||
if err != nil {
|
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 config.SessionCookie{}, fmt.Errorf("session expired due to max lifetime exceeded")
|
return repository.Session{}, fmt.Errorf("session expired due to max lifetime exceeded")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if currentTime > session.Expiry {
|
if currentTime > session.Expiry {
|
||||||
err = auth.queries.DeleteSession(c, cookie)
|
err = auth.queries.DeleteSession(c, cookie)
|
||||||
if err != nil {
|
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 config.SessionCookie{}, fmt.Errorf("session expired")
|
return repository.Session{}, fmt.Errorf("session expired")
|
||||||
}
|
}
|
||||||
|
|
||||||
return config.SessionCookie{
|
return repository.Session{
|
||||||
UUID: session.UUID,
|
UUID: session.UUID,
|
||||||
Username: session.Username,
|
Username: session.Username,
|
||||||
Email: session.Email,
|
Email: session.Email,
|
||||||
@@ -346,6 +361,7 @@ func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie,
|
|||||||
OAuthGroups: session.OAuthGroups,
|
OAuthGroups: session.OAuthGroups,
|
||||||
OAuthName: session.OAuthName,
|
OAuthName: session.OAuthName,
|
||||||
OAuthSub: session.OAuthSub,
|
OAuthSub: session.OAuthSub,
|
||||||
|
LdapGroups: session.LdapGroups,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,18 +371,18 @@ func (auth *AuthService) UserAuthConfigured() bool {
|
|||||||
|
|
||||||
func (auth *AuthService) IsUserAllowed(c *gin.Context, context config.UserContext, acls config.App) bool {
|
func (auth *AuthService) IsUserAllowed(c *gin.Context, context config.UserContext, acls config.App) bool {
|
||||||
if context.OAuth {
|
if context.OAuth {
|
||||||
log.Debug().Msg("Checking OAuth whitelist")
|
tlog.App.Debug().Msg("Checking OAuth whitelist")
|
||||||
return utils.CheckFilter(acls.OAuth.Whitelist, context.Email)
|
return utils.CheckFilter(acls.OAuth.Whitelist, context.Email)
|
||||||
}
|
}
|
||||||
|
|
||||||
if acls.Users.Block != "" {
|
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) {
|
if utils.CheckFilter(acls.Users.Block, context.Username) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg("Checking users")
|
tlog.App.Debug().Msg("Checking users")
|
||||||
return utils.CheckFilter(acls.Users.Allow, context.Username)
|
return utils.CheckFilter(acls.Users.Allow, context.Username)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,19 +393,19 @@ func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context config.UserConte
|
|||||||
|
|
||||||
for id := range config.OverrideProviders {
|
for id := range config.OverrideProviders {
|
||||||
if context.Provider == id {
|
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
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for userGroup := range strings.SplitSeq(context.OAuthGroups, ",") {
|
for userGroup := range strings.SplitSeq(context.OAuthGroups, ",") {
|
||||||
if utils.CheckFilter(requiredGroups, strings.TrimSpace(userGroup)) {
|
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
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg("No groups matched")
|
tlog.App.Debug().Msg("No groups matched")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,7 +442,7 @@ func (auth *AuthService) IsAuthEnabled(uri string, path config.AppPath) (bool, e
|
|||||||
func (auth *AuthService) GetBasicAuth(c *gin.Context) *config.User {
|
func (auth *AuthService) GetBasicAuth(c *gin.Context) *config.User {
|
||||||
username, password, ok := c.Request.BasicAuth()
|
username, password, ok := c.Request.BasicAuth()
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Debug().Msg("No basic auth provided")
|
tlog.App.Debug().Msg("No basic auth provided")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return &config.User{
|
return &config.User{
|
||||||
@@ -443,11 +459,11 @@ func (auth *AuthService) CheckIP(acls config.AppIP, ip string) bool {
|
|||||||
for _, blocked := range blockedIps {
|
for _, blocked := range blockedIps {
|
||||||
res, err := utils.FilterIP(blocked, ip)
|
res, err := utils.FilterIP(blocked, ip)
|
||||||
if err != nil {
|
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
|
continue
|
||||||
}
|
}
|
||||||
if res {
|
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
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -455,21 +471,21 @@ func (auth *AuthService) CheckIP(acls config.AppIP, ip string) bool {
|
|||||||
for _, allowed := range allowedIPs {
|
for _, allowed := range allowedIPs {
|
||||||
res, err := utils.FilterIP(allowed, ip)
|
res, err := utils.FilterIP(allowed, ip)
|
||||||
if err != nil {
|
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
|
continue
|
||||||
}
|
}
|
||||||
if res {
|
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
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(allowedIPs) > 0 {
|
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
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -477,15 +493,15 @@ func (auth *AuthService) IsBypassedIP(acls config.AppIP, ip string) bool {
|
|||||||
for _, bypassed := range acls.Bypass {
|
for _, bypassed := range acls.Bypass {
|
||||||
res, err := utils.FilterIP(bypassed, ip)
|
res, err := utils.FilterIP(bypassed, ip)
|
||||||
if err != nil {
|
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
|
continue
|
||||||
}
|
}
|
||||||
if res {
|
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
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import (
|
|||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/decoders"
|
"github.com/steveiliop56/tinyauth/internal/utils/decoders"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
container "github.com/docker/docker/api/types/container"
|
container "github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type DockerService struct {
|
type DockerService struct {
|
||||||
@@ -37,7 +37,7 @@ func (docker *DockerService) Init() error {
|
|||||||
_, err = docker.client.Ping(docker.context)
|
_, err = docker.client.Ping(docker.context)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug().Err(err).Msg("Docker not connected")
|
tlog.App.Debug().Err(err).Msg("Docker not connected")
|
||||||
docker.isConnected = false
|
docker.isConnected = false
|
||||||
docker.client = nil
|
docker.client = nil
|
||||||
docker.context = nil
|
docker.context = nil
|
||||||
@@ -45,7 +45,7 @@ func (docker *DockerService) Init() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
docker.isConnected = true
|
docker.isConnected = true
|
||||||
log.Debug().Msg("Docker connected")
|
tlog.App.Debug().Msg("Docker connected")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -68,7 +68,7 @@ func (docker *DockerService) inspectContainer(containerId string) (container.Ins
|
|||||||
|
|
||||||
func (docker *DockerService) GetLabels(appDomain string) (config.App, error) {
|
func (docker *DockerService) GetLabels(appDomain string) (config.App, error) {
|
||||||
if !docker.isConnected {
|
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
|
return config.App{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,17 +90,17 @@ func (docker *DockerService) GetLabels(appDomain string) (config.App, error) {
|
|||||||
|
|
||||||
for appName, appLabels := range labels.Apps {
|
for appName, appLabels := range labels.Apps {
|
||||||
if appLabels.Config.Domain == appDomain {
|
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
|
return appLabels, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.SplitN(appDomain, ".", 2)[0] == appName {
|
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
|
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
|
return config.App{}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ func (generic *GenericOAuthService) Userinfo() (config.Claims, error) {
|
|||||||
return user, err
|
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)
|
err = json.Unmarshal(body, &user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/cenkalti/backoff/v5"
|
"github.com/cenkalti/backoff/v5"
|
||||||
ldapgo "github.com/go-ldap/ldap/v3"
|
ldapgo "github.com/go-ldap/ldap/v3"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LdapServiceConfig struct {
|
type LdapServiceConfig struct {
|
||||||
@@ -44,7 +46,7 @@ func (ldap *LdapService) Init() error {
|
|||||||
return fmt.Errorf("failed to initialize LDAP with mTLS authentication: %w", err)
|
return fmt.Errorf("failed to initialize LDAP with mTLS authentication: %w", err)
|
||||||
}
|
}
|
||||||
ldap.cert = &cert
|
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`
|
// TODO: Add optional extra CA certificates, instead of `InsecureSkipVerify`
|
||||||
/*
|
/*
|
||||||
@@ -66,12 +68,12 @@ func (ldap *LdapService) Init() error {
|
|||||||
for range time.Tick(time.Duration(5) * time.Minute) {
|
for range time.Tick(time.Duration(5) * time.Minute) {
|
||||||
err := ldap.heartbeat()
|
err := ldap.heartbeat()
|
||||||
if err != nil {
|
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 {
|
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
|
continue
|
||||||
}
|
}
|
||||||
log.Info().Msg("Successfully reconnected to LDAP server")
|
tlog.App.Info().Msg("Successfully reconnected to LDAP server")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -116,7 +118,7 @@ func (ldap *LdapService) connect() (*ldapgo.Conn, error) {
|
|||||||
return ldap.conn, nil
|
return ldap.conn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ldap *LdapService) Search(username string) (string, error) {
|
func (ldap *LdapService) GetUserDN(username string) (string, error) {
|
||||||
// Escape the username to prevent LDAP injection
|
// Escape the username to prevent LDAP injection
|
||||||
escapedUsername := ldapgo.EscapeFilter(username)
|
escapedUsername := ldapgo.EscapeFilter(username)
|
||||||
filter := fmt.Sprintf(ldap.config.SearchFilter, escapedUsername)
|
filter := fmt.Sprintf(ldap.config.SearchFilter, escapedUsername)
|
||||||
@@ -145,6 +147,47 @@ func (ldap *LdapService) Search(username string) (string, error) {
|
|||||||
return userDN, nil
|
return userDN, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ldap *LdapService) GetUserGroups(userDN string) ([]string, error) {
|
||||||
|
searchRequest := ldapgo.NewSearchRequest(
|
||||||
|
ldap.config.BaseDN,
|
||||||
|
ldapgo.ScopeWholeSubtree, ldapgo.NeverDerefAliases, 0, 0, false,
|
||||||
|
"(objectclass=groupOfUniqueNames)",
|
||||||
|
[]string{"uniquemember"},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
ldap.mutex.Lock()
|
||||||
|
defer ldap.mutex.Unlock()
|
||||||
|
|
||||||
|
searchResult, err := ldap.conn.Search(searchRequest)
|
||||||
|
if err != nil {
|
||||||
|
return []string{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
groupDNs := []string{}
|
||||||
|
|
||||||
|
for _, entry := range searchResult.Entries {
|
||||||
|
memberAttributes := entry.GetAttributeValues("uniquemember")
|
||||||
|
// no need to escape username here, if it's malicious it won't match anything
|
||||||
|
if slices.Contains(memberAttributes, userDN) {
|
||||||
|
groupDNs = append(groupDNs, entry.DN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should work for most ldap providers?
|
||||||
|
groups := []string{}
|
||||||
|
|
||||||
|
for _, groupDN := range groupDNs {
|
||||||
|
groupDN = strings.TrimPrefix(groupDN, "cn=")
|
||||||
|
parts := strings.SplitN(groupDN, ",", 2)
|
||||||
|
if len(parts) > 0 {
|
||||||
|
groups = append(groups, parts[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ldap *LdapService) BindService(rebind bool) error {
|
func (ldap *LdapService) BindService(rebind bool) error {
|
||||||
// Locks must not be used for initial binding attempt
|
// Locks must not be used for initial binding attempt
|
||||||
if rebind {
|
if rebind {
|
||||||
@@ -169,7 +212,7 @@ func (ldap *LdapService) Bind(userDN string, password string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ldap *LdapService) heartbeat() error {
|
func (ldap *LdapService) heartbeat() error {
|
||||||
log.Debug().Msg("Performing LDAP connection heartbeat")
|
tlog.App.Debug().Msg("Performing LDAP connection heartbeat")
|
||||||
|
|
||||||
searchRequest := ldapgo.NewSearchRequest(
|
searchRequest := ldapgo.NewSearchRequest(
|
||||||
"",
|
"",
|
||||||
@@ -191,7 +234,7 @@ func (ldap *LdapService) heartbeat() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ldap *LdapService) reconnect() 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 := backoff.NewExponentialBackOff()
|
||||||
exp.InitialInterval = 500 * time.Millisecond
|
exp.InitialInterval = 500 * time.Millisecond
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -49,10 +49,10 @@ func (broker *OAuthBrokerService) Init() error {
|
|||||||
for name, service := range broker.services {
|
for name, service := range broker.services {
|
||||||
err := service.Init()
|
err := service.Init()
|
||||||
if err != nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
log.Info().Str("service", name).Msg("Initialized OAuth service")
|
tlog.App.Info().Str("service", name).Msg("Initialized OAuth service")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
39
internal/utils/tlog/log_audit.go
Normal file
39
internal/utils/tlog/log_audit.go
Normal 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()
|
||||||
|
}
|
||||||
86
internal/utils/tlog/log_wrapper.go
Normal file
86
internal/utils/tlog/log_wrapper.go
Normal 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
|
||||||
|
}
|
||||||
93
internal/utils/tlog/log_wrapper_test.go
Normal file
93
internal/utils/tlog/log_wrapper_test.go
Normal 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"])
|
||||||
|
}
|
||||||
@@ -10,9 +10,10 @@ INSERT INTO sessions (
|
|||||||
"expiry",
|
"expiry",
|
||||||
"created_at",
|
"created_at",
|
||||||
"oauth_name",
|
"oauth_name",
|
||||||
"oauth_sub"
|
"oauth_sub",
|
||||||
|
"ldap_groups"
|
||||||
) VALUES (
|
) VALUES (
|
||||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||||
)
|
)
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
@@ -34,7 +35,8 @@ UPDATE "sessions" SET
|
|||||||
"oauth_groups" = ?,
|
"oauth_groups" = ?,
|
||||||
"expiry" = ?,
|
"expiry" = ?,
|
||||||
"oauth_name" = ?,
|
"oauth_name" = ?,
|
||||||
"oauth_sub" = ?
|
"oauth_sub" = ?,
|
||||||
|
"ldap_groups" = ?
|
||||||
WHERE "uuid" = ?
|
WHERE "uuid" = ?
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
|
|||||||
@@ -9,5 +9,6 @@ CREATE TABLE IF NOT EXISTS "sessions" (
|
|||||||
"expiry" INTEGER NOT NULL,
|
"expiry" INTEGER NOT NULL,
|
||||||
"created_at" INTEGER NOT NULL,
|
"created_at" INTEGER NOT NULL,
|
||||||
"oauth_name" TEXT NULL,
|
"oauth_name" TEXT NULL,
|
||||||
"oauth_sub" TEXT NULL
|
"oauth_sub" TEXT NULL,
|
||||||
|
"ldap_groups" TEXT NULL
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user