From c4529be557253d0faeba6cd422455fdfeca41763 Mon Sep 17 00:00:00 2001 From: Stavros Date: Sun, 21 Dec 2025 11:21:11 +0200 Subject: [PATCH] feat: add experimental config file support --- .env.example | 2 + .gitignore | 9 ++- .other/config.example.yaml | 53 ---------------- Dockerfile | 4 ++ Dockerfile.dev | 4 ++ Dockerfile.distroless | 5 +- cmd/tinyauth.go | 46 +++++++++----- config.example.yaml | 88 +++++++++++++++++++++++++++ internal/config/config.go | 72 ++++++++++++---------- internal/utils/loaders/loader_file.go | 35 +++++++++++ 10 files changed, 215 insertions(+), 103 deletions(-) delete mode 100644 .other/config.example.yaml create mode 100644 config.example.yaml create mode 100644 internal/utils/loaders/loader_file.go diff --git a/.env.example b/.env.example index 63bddef..62ed026 100644 --- a/.env.example +++ b/.env.example @@ -14,6 +14,8 @@ TINYAUTH_DISABLEANALYTICS=false TINYAUTH_DISABLERESOURCES=false # Disable UI warning messages TINYAUTH_DISABLEUIWARNINGS=false +# Enable JSON formatted logs +TINYAUTH_LOGJSON=false # Server Configuration diff --git a/.gitignore b/.gitignore index cb79b93..8eead11 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,11 @@ tmp internal/assets/version # data directory -data \ No newline at end of file +data + +# config file +config.yml + +# binary out +tinyauth.db +resources \ No newline at end of file diff --git a/.other/config.example.yaml b/.other/config.example.yaml deleted file mode 100644 index ecf0eff..0000000 --- a/.other/config.example.yaml +++ /dev/null @@ -1,53 +0,0 @@ -app_url: "https://tinyauth.example.com" -log_level: "info" -resources_dir: "/etc/tinyauth/resources" -database_path: "/var/lib/tinyauth/tinyauth.db" -disable_analytics: false -disable_resources: false -disable_ui_warnings: false - -server: - port: 8080 - address: "0.0.0.0" - socket_path: "/var/run/tinyauth.sock" - trusted_proxies: "10.10.10.0/24" - -auth: - users: "user:hash" - users_file: "/etc/tinyauth/users.yaml" - secure_cookie: true - session_expiry: 3600 - login_timeout: 300 - login_max_retries: 5 - -oauth: - whitelist: "example.com" - auto_redirect: "pocketid" - providers: - google: - client_id: "some-client-id" - client_secret: "some-client-secret" - client_secret_file: "some-client-secret-file" - scopes: - - "openid" - - "email" - - "profile" - redirect_url: "https://tinyauth.example.com/oauth/callback/google" - auth_url: "https://accounts.google.com/o/oauth2/auth" - token_url: "https://oauth2.googleapis.com/token" - user_info_url: "https://www.googleapis.com/oauth2/v3/userinfo" - insecure: false - name: "Google" - -ui: - title: "Tinyauth" - forgot_password_message: "Contact your administrator to reset your password." - background_image: "/static/background.jpg" - -ldap: - address: "ldap.example.com:389" - base_dn: "dc=example,dc=com" - bind_dn: "cn=admin,dc=example,dc=com" - bind_password: "password" - search_filter: "(uid={username})" - insecure: false diff --git a/Dockerfile b/Dockerfile index 0504c58..c015d15 100644 --- a/Dockerfile +++ b/Dockerfile @@ -52,6 +52,10 @@ EXPOSE 3000 VOLUME ["/data"] +ENV DATABASEPATH=/data/tinyauth.db + +ENV RESOURCESDIR=/data/resources + ENV GIN_MODE=release ENV PATH=$PATH:/tinyauth diff --git a/Dockerfile.dev b/Dockerfile.dev index cdc6fe0..d7f2c1d 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -16,4 +16,8 @@ COPY ./air.toml ./ EXPOSE 3000 +ENV DATABASEPATH=/data/tinyauth.db + +ENV RESOURCESDIR=/data/resources + ENTRYPOINT ["air", "-c", "air.toml"] \ No newline at end of file diff --git a/Dockerfile.distroless b/Dockerfile.distroless index 2a72617..04b19d3 100644 --- a/Dockerfile.distroless +++ b/Dockerfile.distroless @@ -33,7 +33,6 @@ COPY go.sum ./ RUN go mod download -COPY ./main.go ./ COPY ./cmd ./cmd COPY ./internal ./internal COPY --from=frontend-builder /frontend/dist ./internal/assets/dist @@ -56,6 +55,10 @@ EXPOSE 3000 VOLUME ["/data"] +ENV DATABASEPATH=/data/tinyauth.db + +ENV RESOURCESDIR=/data/resources + ENV GIN_MODE=release ENV PATH=$PATH:/tinyauth diff --git a/cmd/tinyauth.go b/cmd/tinyauth.go index b304dd4..8642eed 100644 --- a/cmd/tinyauth.go +++ b/cmd/tinyauth.go @@ -14,17 +14,28 @@ import ( "github.com/traefik/paerser/cli" ) -type TinyauthCmdConfiguration struct { - config.Config - // ConfigFile string `description:"Path to config file."` -} - -func NewTinyauthCmdConfiguration() *TinyauthCmdConfiguration { - return &TinyauthCmdConfiguration{ - Config: config.Config{ - LogLevel: "info", +func NewTinyauthCmdConfiguration() *config.Config { + return &config.Config{ + LogLevel: "info", + ResourcesDir: "./resources", + DatabasePath: "./tinyauth.db", + Server: config.ServerConfig{ + Port: 3000, + Address: "0.0.0.0", + }, + Auth: config.AuthConfig{ + SessionExpiry: 3600, + LoginTimeout: 300, + LoginMaxRetries: 3, + }, + UI: config.UIConfig{ + Title: "Tinyauth", + ForgotPasswordMessage: "You can change your password by changing the configuration.", + BackgroundImage: "/background.jpg", + }, + Experimental: config.ExperimentalConfig{ + ConfigFile: "", }, - // ConfigFile: "", } } @@ -32,8 +43,9 @@ func main() { tConfig := NewTinyauthCmdConfiguration() loaders := []cli.ResourceLoader{ - &loaders.EnvLoader{}, + &loaders.FileLoader{}, &loaders.FlagLoader{}, + &loaders.EnvLoader{}, } cmdTinyauth := &cli.Command{ @@ -42,7 +54,7 @@ func main() { Configuration: tConfig, Resources: loaders, Run: func(_ []string) error { - return runCmd(&tConfig.Config) + return runCmd(*tConfig) }, } @@ -83,7 +95,7 @@ func main() { } } -func runCmd(cfg *config.Config) error { +func runCmd(cfg config.Config) error { logLevel, err := zerolog.ParseLevel(strings.ToLower(cfg.LogLevel)) if err != nil { @@ -92,11 +104,15 @@ func runCmd(cfg *config.Config) error { zerolog.SetGlobalLevel(logLevel) } - log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Caller().Logger() + 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() diff --git a/config.example.yaml b/config.example.yaml new file mode 100644 index 0000000..544bc83 --- /dev/null +++ b/config.example.yaml @@ -0,0 +1,88 @@ +# Tinyauth Example Configuration + +# The base URL where Tinyauth is accessible +appUrl: "https://auth.example.com" +# Log level: trace, debug, info, warn, error +logLevel: "info" +# Directory for static resources +resourcesDir: "./resources" +# Path to SQLite database file +databasePath: "./tinyauth.db" +# Disable usage analytics +disableAnalytics: false +# Disable static resource serving +disableResources: false +# Disable UI warning messages +disableUIWarnings: false +# Enable JSON formatted logs +logJSON: false + +# Server Configuration +server: + # Port to listen on + port: 3000 + # Interface to bind to (0.0.0.0 for all interfaces) + address: "0.0.0.0" + # Unix socket path (optional, overrides port/address if set) + socketPath: "" + # Comma-separated list of trusted proxy IPs/CIDRs + trustedProxies: "" + +# Authentication Configuration +auth: + # Format: username:bcrypt_hash (use bcrypt to generate hash) + users: "admin:$2a$10$example_bcrypt_hash_here" + # Path to external users file (optional) + usersFile: "" + # Enable secure cookies (requires HTTPS) + secureCookie: false + # Session expiry in seconds (3600 = 1 hour) + sessionExpiry: 3600 + # Login timeout in seconds (300 = 5 minutes) + loginTimeout: 300 + # Maximum login retries before lockout + loginMaxRetries: 3 + +# OAuth Configuration +oauth: + # Regex pattern for allowed email addresses (e.g., /@example\.com$/) + whitelist: "" + # Provider ID to auto-redirect to (skips login page) + autoRedirect: "" + # OAuth Provider Configuration (replace myprovider with your provider name) + providers: + myprovider: + clientId: "your_client_id_here" + clientSecret: "your_client_secret_here" + authUrl: "https://provider.example.com/oauth/authorize" + tokenUrl: "https://provider.example.com/oauth/token" + userInfoUrl: "https://provider.example.com/oauth/userinfo" + redirectUrl: "https://auth.example.com/api/oauth/callback/myprovider" + scopes: "openid email profile" + name: "My OAuth Provider" + # Allow insecure connections (self-signed certificates) + insecure: false + +# UI Customization +ui: + # Custom title for login page + title: "Tinyauth" + # Message shown on forgot password page + forgotPasswordMessage: "Contact your administrator to reset your password" + # Background image URL for login page + backgroundImage: "" + +# LDAP Configuration (optional) +ldap: + # LDAP server address + address: "ldap://ldap.example.com:389" + # DN for binding to LDAP server + bindDn: "cn=readonly,dc=example,dc=com" + # Password for bind DN + bindPassword: "your_bind_password" + # Base DN for user searches + baseDn: "dc=example,dc=com" + # Search filter (%s will be replaced with username) + searchFilter: "(&(uid=%s)(memberOf=cn=users,ou=groups,dc=example,dc=com))" + # Allow insecure LDAP connections + insecure: false diff --git a/internal/config/config.go b/internal/config/config.go index 30db258..f4bb484 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -15,55 +15,61 @@ var RedirectCookieName = "tinyauth-redirect" // Main app config type Config struct { - AppURL string `description:"The base URL where the app is hosted."` - LogLevel string `description:"Log level (trace, debug, info, warn, error)."` - ResourcesDir string `description:"The directory where resources are stored."` - DatabasePath string `description:"The path to the database file."` - DisableAnalytics bool `description:"Disable analytics."` - DisableResources bool `description:"Disable resources server."` - DisableUIWarnings bool `description:"Disable UI warnings."` - Server ServerConfig - Auth AuthConfig - OAuth OAuthConfig - UI UIConfig - Ldap LdapConfig + AppURL string `description:"The base URL where the app is hosted." yaml:"appUrl"` + LogLevel string `description:"Log level (trace, debug, info, warn, error)." yaml:"logLevel"` + ResourcesDir string `description:"The directory where resources are stored." yaml:"resourcesDir"` + DatabasePath string `description:"The path to the database file." yaml:"databasePath"` + DisableAnalytics bool `description:"Disable analytics." yaml:"disableAnalytics"` + DisableResources bool `description:"Disable resources server." yaml:"disableResources"` + DisableUIWarnings bool `description:"Disable UI warnings." yaml:"disableUIWarnings"` + LogJSON bool `description:"Enable JSON formatted logs." yaml:"logJSON"` + Server ServerConfig `description:"Server configuration." yaml:"server"` + Auth AuthConfig `description:"Authentication configuration." yaml:"auth"` + OAuth OAuthConfig `description:"OAuth configuration." yaml:"oauth"` + UI UIConfig `description:"UI customization." yaml:"ui"` + Ldap LdapConfig `description:"LDAP configuration." yaml:"ldap"` + Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental"` } type ServerConfig struct { - Port int `description:"The port on which the server listens."` - Address string `description:"The address on which the server listens."` - SocketPath string `description:"The path to the Unix socket."` - TrustedProxies string `description:"Comma-separated list of trusted proxy addresses."` + Port int `description:"The port on which the server listens." yaml:"port"` + Address string `description:"The address on which the server listens." yaml:"address"` + SocketPath string `description:"The path to the Unix socket." yaml:"socketPath"` + TrustedProxies string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies"` } type AuthConfig struct { - Users string `description:"Comma-separated list of users (username:hashed_password)."` - UsersFile string `description:"Path to the users file."` - SecureCookie bool `description:"Enable secure cookies."` - SessionExpiry int `description:"Session expiry time in seconds."` - LoginTimeout int `description:"Login timeout in seconds."` - LoginMaxRetries int `description:"Maximum login retries."` + Users string `description:"Comma-separated list of users (username:hashed_password)." yaml:"users"` + UsersFile string `description:"Path to the users file." yaml:"usersFile"` + SecureCookie bool `description:"Enable secure cookies." yaml:"secureCookie"` + SessionExpiry int `description:"Session expiry time in seconds." yaml:"sessionExpiry"` + LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout"` + LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries"` } type OAuthConfig struct { - Whitelist string `description:"Comma-separated list of allowed OAuth domains."` - AutoRedirect string `description:"The OAuth provider to use for automatic redirection."` + Whitelist string `description:"Comma-separated list of allowed OAuth domains." yaml:"whitelist"` + AutoRedirect string `description:"The OAuth provider to use for automatic redirection." yaml:"autoRedirect"` Providers map[string]OAuthServiceConfig } type UIConfig struct { - Title string `description:"The title of the UI."` - ForgotPasswordMessage string `description:"Message displayed on the forgot password page."` - BackgroundImage string `description:"Path to the background image."` + Title string `description:"The title of the UI." yaml:"title"` + ForgotPasswordMessage string `description:"Message displayed on the forgot password page." yaml:"forgotPasswordMessage"` + BackgroundImage string `description:"Path to the background image." yaml:"backgroundImage"` } type LdapConfig struct { - Address string `description:"LDAP server address."` - BindDN string `description:"Bind DN for LDAP authentication."` - BindPassword string `description:"Bind password for LDAP authentication."` - BaseDN string `description:"Base DN for LDAP searches."` - Insecure bool `description:"Allow insecure LDAP connections."` - SearchFilter string `description:"LDAP search filter."` + Address string `description:"LDAP server address." yaml:"address"` + BindDN string `description:"Bind DN for LDAP authentication." yaml:"bindDn"` + BindPassword string `description:"Bind password for LDAP authentication." yaml:"bindPassword"` + BaseDN string `description:"Base DN for LDAP searches." yaml:"baseDn"` + Insecure bool `description:"Allow insecure LDAP connections." yaml:"insecure"` + SearchFilter string `description:"LDAP search filter." yaml:"searchFilter"` +} + +type ExperimentalConfig struct { + ConfigFile string `description:"Path to config file." yaml:"-"` } // Config loader options diff --git a/internal/utils/loaders/loader_file.go b/internal/utils/loaders/loader_file.go new file mode 100644 index 0000000..7242791 --- /dev/null +++ b/internal/utils/loaders/loader_file.go @@ -0,0 +1,35 @@ +package loaders + +import ( + "github.com/rs/zerolog/log" + "github.com/traefik/paerser/cli" + "github.com/traefik/paerser/file" + "github.com/traefik/paerser/flag" +) + +type FileLoader struct{} + +func (f *FileLoader) Load(args []string, cmd *cli.Command) (bool, error) { + flags, err := flag.Parse(args, cmd.Configuration) + + if err != nil { + return false, err + } + + // I guess we are using traefik as the root name + configFileFlag := "traefik.experimental.configFile" + + if _, ok := flags[configFileFlag]; !ok { + return false, nil + } + + log.Warn().Msg("Using experimental file config loader, this feature is experimental and may change or be removed in future releases") + + err = file.Decode(flags[configFileFlag], cmd.Configuration) + + if err != nil { + return false, err + } + + return true, nil +}