diff --git a/.env.example b/.env.example index 905775e..091662f 100644 --- a/.env.example +++ b/.env.example @@ -1,99 +1,221 @@ -# Base Configuration +# Tinyauth example configuration -# The base URL where Tinyauth is accessible -TINYAUTH_APPURL="https://auth.example.com" -# Directory for static resources -TINYAUTH_RESOURCESDIR="/data/resources" -# Path to SQLite database file -TINYAUTH_DATABASEPATH="/data/tinyauth.db" -# Disable version heartbeat -TINYAUTH_DISABLEANALYTICS="false" -# Disable static resource serving -TINYAUTH_DISABLERESOURCES="false" +# The base URL where the app is hosted. +TINYAUTH_APPURL= -# Logging Configuration +# The directory where resources are stored. +TINYAUTH_RESOURCESDIR="./resources" -# Log level: trace, debug, info, warn, error -TINYAUTH_LOG_LEVEL="info" -# Enable JSON formatted logs -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" +# The path to the database file. +TINYAUTH_DATABASEPATH="./tinyauth.db" -# Server Configuration +# Disable analytics. +TINYAUTH_DISABLEANALYTICS=false -# Port to listen on -TINYAUTH_SERVER_PORT="3000" -# Interface to bind to (0.0.0.0 for all interfaces) +# Disable resources server. +TINYAUTH_DISABLERESOURCES=false + +# The port on which the server listens. +TINYAUTH_SERVER_PORT=3000 + +# The address on which the server listens. TINYAUTH_SERVER_ADDRESS="0.0.0.0" -# Unix socket path (optional, overrides port/address if set) -TINYAUTH_SERVER_SOCKETPATH="" -# Authentication Configuration +# The path to the Unix socket. +TINYAUTH_SERVER_SOCKETPATH= -# Format: username:bcrypt_hash (use bcrypt to generate hash) -TINYAUTH_AUTH_USERS="admin:$2a$10$example_bcrypt_hash_here" -# Path to external users file (optional) -TINYAUTH_AUTH_USERSFILE="" -# Enable secure cookies (requires HTTPS) -TINYAUTH_AUTH_SECURECOOKIE="true" -# Session expiry in seconds (7200 = 2 hours) -TINYAUTH_AUTH_SESSIONEXPIRY="7200" -# Session maximum lifetime in seconds (0 = unlimited) -TINYAUTH_AUTH_SESSIONMAXLIFETIME="0" -# Login timeout in seconds (300 = 5 minutes) -TINYAUTH_AUTH_LOGINTIMEOUT="300" -# Maximum login retries before lockout -TINYAUTH_AUTH_LOGINMAXRETRIES="5" -# Comma-separated list of trusted proxy IPs/CIDRs -TINYAUTH_AUTH_TRUSTEDPROXIES="" +# List of allowed IPs or CIDR ranges. +TINYAUTH_AUTH_IP_ALLOW= -# OAuth Configuration +# List of blocked IPs or CIDR ranges. +TINYAUTH_AUTH_IP_BLOCK= -# Regex pattern for allowed email addresses (e.g., /@example\.com$/) -TINYAUTH_OAUTH_WHITELIST="" -# Provider ID to auto-redirect to (skips login page) -TINYAUTH_OAUTH_AUTOREDIRECT="" -# OAuth Provider Configuration (replace MYPROVIDER with your provider name) -TINYAUTH_OAUTH_PROVIDERS_MYPROVIDER_CLIENTID="your_client_id_here" -TINYAUTH_OAUTH_PROVIDERS_MYPROVIDER_CLIENTSECRET="your_client_secret_here" -TINYAUTH_OAUTH_PROVIDERS_MYPROVIDER_AUTHURL="https://provider.example.com/oauth/authorize" -TINYAUTH_OAUTH_PROVIDERS_MYPROVIDER_TOKENURL="https://provider.example.com/oauth/token" -TINYAUTH_OAUTH_PROVIDERS_MYPROVIDER_USERINFOURL="https://provider.example.com/oauth/userinfo" -TINYAUTH_OAUTH_PROVIDERS_MYPROVIDER_REDIRECTURL="https://auth.example.com/oauth/callback/myprovider" -TINYAUTH_OAUTH_PROVIDERS_MYPROVIDER_SCOPES="openid email profile" -TINYAUTH_OAUTH_PROVIDERS_MYPROVIDER_NAME="My OAuth Provider" -# Allow self-signed certificates -TINYAUTH_OAUTH_PROVIDERS_MYPROVIDER_INSECURE="false" +# Comma-separated list of users (username:hashed_password). +TINYAUTH_AUTH_USERS= -# UI Customization +# Path to the users file. +TINYAUTH_AUTH_USERSFILE= -# Custom title for login page +# Enable secure cookies. +TINYAUTH_AUTH_SECURECOOKIE=false + +# Session expiry time in seconds. +TINYAUTH_AUTH_SESSIONEXPIRY=86400 + +# Maximum session lifetime in seconds. +TINYAUTH_AUTH_SESSIONMAXLIFETIME=0 + +# Login timeout in seconds. +TINYAUTH_AUTH_LOGINTIMEOUT=300 + +# Maximum login retries. +TINYAUTH_AUTH_LOGINMAXRETRIES=3 + +# Comma-separated list of trusted proxy addresses. +TINYAUTH_AUTH_TRUSTEDPROXIES= + +# The domain of the app. +TINYAUTH_APPS_name_CONFIG_DOMAIN= + +# Comma-separated list of allowed users. +TINYAUTH_APPS_name_USERS_ALLOW= + +# Comma-separated list of blocked users. +TINYAUTH_APPS_name_USERS_BLOCK= + +# Comma-separated list of allowed OAuth groups. +TINYAUTH_APPS_name_OAUTH_WHITELIST= + +# Comma-separated list of required OAuth groups. +TINYAUTH_APPS_name_OAUTH_GROUPS= + +# List of allowed IPs or CIDR ranges. +TINYAUTH_APPS_name_IP_ALLOW= + +# List of blocked IPs or CIDR ranges. +TINYAUTH_APPS_name_IP_BLOCK= + +# List of IPs or CIDR ranges that bypass authentication. +TINYAUTH_APPS_name_IP_BYPASS= + +# Custom headers to add to the response. +TINYAUTH_APPS_name_RESPONSE_HEADERS= + +# Basic auth username. +TINYAUTH_APPS_name_RESPONSE_BASICAUTH_USERNAME= + +# Basic auth password. +TINYAUTH_APPS_name_RESPONSE_BASICAUTH_PASSWORD= + +# Path to the file containing the basic auth password. +TINYAUTH_APPS_name_RESPONSE_BASICAUTH_PASSWORDFILE= + +# Comma-separated list of allowed paths. +TINYAUTH_APPS_name_PATH_ALLOW= + +# Comma-separated list of blocked paths. +TINYAUTH_APPS_name_PATH_BLOCK= + +# Comma-separated list of required LDAP groups. +TINYAUTH_APPS_name_LDAP_GROUPS= + +# Comma-separated list of allowed OAuth domains. +TINYAUTH_OAUTH_WHITELIST= + +# The OAuth provider to use for automatic redirection. +TINYAUTH_OAUTH_AUTOREDIRECT= + +# OAuth client ID. +TINYAUTH_OAUTH_PROVIDERS_name_CLIENTID= + +# OAuth client secret. +TINYAUTH_OAUTH_PROVIDERS_name_CLIENTSECRET= + +# Path to the file containing the OAuth client secret. +TINYAUTH_OAUTH_PROVIDERS_name_CLIENTSECRETFILE= + +# OAuth scopes. +TINYAUTH_OAUTH_PROVIDERS_name_SCOPES= + +# OAuth redirect URL. +TINYAUTH_OAUTH_PROVIDERS_name_REDIRECTURL= + +# OAuth authorization URL. +TINYAUTH_OAUTH_PROVIDERS_name_AUTHURL= + +# OAuth token URL. +TINYAUTH_OAUTH_PROVIDERS_name_TOKENURL= + +# OAuth userinfo URL. +TINYAUTH_OAUTH_PROVIDERS_name_USERINFOURL= + +# Allow insecure OAuth connections. +TINYAUTH_OAUTH_PROVIDERS_name_INSECURE=false + +# Provider name in UI. +TINYAUTH_OAUTH_PROVIDERS_name_NAME= + +# Path to the private key file. +TINYAUTH_OIDC_PRIVATEKEYPATH="./tinyauth_oidc_key" + +# Path to the public key file. +TINYAUTH_OIDC_PUBLICKEYPATH="./tinyauth_oidc_key.pub" + +# OIDC client ID. +TINYAUTH_OIDC_CLIENTS_name_CLIENTID= + +# OIDC client secret. +TINYAUTH_OIDC_CLIENTS_name_CLIENTSECRET= + +# Path to the file containing the OIDC client secret. +TINYAUTH_OIDC_CLIENTS_name_CLIENTSECRETFILE= + +# List of trusted redirect URIs. +TINYAUTH_OIDC_CLIENTS_name_TRUSTEDREDIRECTURIS= + +# Client name in UI. +TINYAUTH_OIDC_CLIENTS_name_NAME= + +# The title of the UI. TINYAUTH_UI_TITLE="Tinyauth" -# Message shown on forgot password page -TINYAUTH_UI_FORGOTPASSWORDMESSAGE="Contact your administrator to reset your password" -# Background image URL for login page -TINYAUTH_UI_BACKGROUNDIMAGE="" -# Disable UI warning messages -TINYAUTH_UI_DISABLEWARNINGS="false" -# LDAP Configuration +# Message displayed on the forgot password page. +TINYAUTH_UI_FORGOTPASSWORDMESSAGE="You can change your password by changing the configuration." + +# Path to the background image. +TINYAUTH_UI_BACKGROUNDIMAGE="/background.jpg" + +# Disable UI warnings. +TINYAUTH_UI_DISABLEWARNINGS=false + +# LDAP server address. +TINYAUTH_LDAP_ADDRESS= + +# Bind DN for LDAP authentication. +TINYAUTH_LDAP_BINDDN= + +# Bind password for LDAP authentication. +TINYAUTH_LDAP_BINDPASSWORD= + +# Base DN for LDAP searches. +TINYAUTH_LDAP_BASEDN= + +# Allow insecure LDAP connections. +TINYAUTH_LDAP_INSECURE=false + +# LDAP search filter. +TINYAUTH_LDAP_SEARCHFILTER="(uid=%s)" + +# Certificate for mTLS authentication. +TINYAUTH_LDAP_AUTHCERT= + +# Certificate key for mTLS authentication. +TINYAUTH_LDAP_AUTHKEY= + +# Cache duration for LDAP group membership in seconds. +TINYAUTH_LDAP_GROUPCACHETTL=900 + +# Log level (trace, debug, info, warn, error). +TINYAUTH_LOG_LEVEL="info" + +# Enable JSON formatted logs. +TINYAUTH_LOG_JSON=false + +# Enable this log stream. +TINYAUTH_LOG_STREAMS_HTTP_ENABLED=true + +# Log level for this stream. Use global if empty. +TINYAUTH_LOG_STREAMS_HTTP_LEVEL= + +# Enable this log stream. +TINYAUTH_LOG_STREAMS_APP_ENABLED=true + +# Log level for this stream. Use global if empty. +TINYAUTH_LOG_STREAMS_APP_LEVEL= + +# Enable this log stream. +TINYAUTH_LOG_STREAMS_AUDIT_ENABLED=false + +# Log level for this stream. Use global if empty. +TINYAUTH_LOG_STREAMS_AUDIT_LEVEL= -# LDAP server address -TINYAUTH_LDAP_ADDRESS="ldap://ldap.example.com:389" -# DN for binding to LDAP server -TINYAUTH_LDAP_BINDDN="cn=readonly,dc=example,dc=com" -# Password for bind DN -TINYAUTH_LDAP_BINDPASSWORD="your_bind_password" -# Base DN for user searches -TINYAUTH_LDAP_BASEDN="dc=example,dc=com" -# Search filter (%s will be replaced with username) -TINYAUTH_LDAP_SEARCHFILTER="(&(uid=%s)(memberOf=cn=users,ou=groups,dc=example,dc=com))" -# Allow insecure LDAP connections -TINYAUTH_LDAP_INSECURE="false" diff --git a/Makefile b/Makefile index 4ac08c4..dfabc34 100644 --- a/Makefile +++ b/Makefile @@ -60,11 +60,11 @@ test: go test -v ./... # Development -develop: +dev: docker compose -f $(DEV_COMPOSE) up --force-recreate --pull=always --remove-orphans --build # Development - Infisical -develop-infisical: +dev-infisical: infisical run --env=dev -- docker compose -f $(DEV_COMPOSE) up --force-recreate --pull=always --remove-orphans --build # Production @@ -79,3 +79,7 @@ prod-infisical: .PHONY: sql sql: sqlc generate + +# Go gen +generate: + go run ./gen diff --git a/cmd/tinyauth/tinyauth.go b/cmd/tinyauth/tinyauth.go index 5516c6b..a6cb93e 100644 --- a/cmd/tinyauth/tinyauth.go +++ b/cmd/tinyauth/tinyauth.go @@ -12,60 +12,8 @@ import ( "github.com/traefik/paerser/cli" ) -func NewTinyauthCmdConfiguration() *config.Config { - return &config.Config{ - ResourcesDir: "./resources", - DatabasePath: "./tinyauth.db", - Server: config.ServerConfig{ - Port: 3000, - Address: "0.0.0.0", - }, - Auth: config.AuthConfig{ - SessionExpiry: 86400, // 1 day - SessionMaxLifetime: 0, // disabled - LoginTimeout: 300, // 5 minutes - LoginMaxRetries: 3, - }, - UI: config.UIConfig{ - Title: "Tinyauth", - ForgotPasswordMessage: "You can change your password by changing the configuration.", - BackgroundImage: "/background.jpg", - }, - Ldap: config.LdapConfig{ - Insecure: false, - SearchFilter: "(uid=%s)", - GroupCacheTTL: 900, // 15 minutes - }, - 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: "", - }, - }, - }, - OIDC: config.OIDCConfig{ - PrivateKeyPath: "./tinyauth_oidc_key", - PublicKeyPath: "./tinyauth_oidc_key.pub", - }, - Experimental: config.ExperimentalConfig{ - ConfigFile: "", - }, - } -} - func main() { - tConfig := NewTinyauthCmdConfiguration() + tConfig := config.NewDefaultConfiguration() loaders := []cli.ResourceLoader{ &loaders.FileLoader{}, diff --git a/gen/gen.go b/gen/gen.go new file mode 100644 index 0000000..520eec8 --- /dev/null +++ b/gen/gen.go @@ -0,0 +1,11 @@ +package main + +import ( + "log/slog" +) + +func main() { + slog.Info("generating example env file") + + generateExampleEnv() +} diff --git a/gen/gen_env.go b/gen/gen_env.go new file mode 100644 index 0000000..8db51c9 --- /dev/null +++ b/gen/gen_env.go @@ -0,0 +1,137 @@ +package main + +import ( + "bytes" + "fmt" + "log/slog" + "os" + "reflect" + "strings" + + "github.com/steveiliop56/tinyauth/internal/config" +) + +type Path struct { + Name string + Description string + Value any +} + +func generateExampleEnv() { + cfg := config.NewDefaultConfiguration() + paths := make([]Path, 0) + + root := reflect.TypeOf(cfg).Elem() + rootValue := reflect.ValueOf(cfg).Elem() + rootPath := "TINYAUTH_" + + buildPaths(root, rootValue, rootPath, &paths) + compiled := compileEnv(paths) + + err := os.Remove(".env.example") + if err != nil { + slog.Error("failed to remove example env file", "error", err) + os.Exit(1) + } + + err = os.WriteFile(".env.example", compiled, 0644) + if err != nil { + slog.Error("failed to write example env file", "error", err) + os.Exit(1) + } +} + +func buildPaths(parent reflect.Type, parentValue reflect.Value, parentPath string, paths *[]Path) { + for i := 0; i < parent.NumField(); i++ { + field := parent.Field(i) + fieldType := field.Type + fieldValue := parentValue.Field(i) + switch fieldType.Kind() { + case reflect.Struct: + childPath := parentPath + strings.ToUpper(field.Name) + "_" + buildPaths(fieldType, fieldValue, childPath, paths) + case reflect.Map: + buildMapPaths(field, parentPath, paths) + case reflect.Bool, reflect.String, reflect.Slice, reflect.Int: + buildPath(field, fieldValue, parentPath, paths) + default: + slog.Info("unknown type", "type", fieldType.Kind()) + } + } +} + +func buildPath(field reflect.StructField, fieldValue reflect.Value, parent string, paths *[]Path) { + desc := field.Tag.Get("description") + yamlTag := field.Tag.Get("yaml") + + // probably internal logic, should be skipped + if yamlTag == "-" { + return + } + + defaultValue := fieldValue.Interface() + + path := Path{ + Name: parent + strings.ToUpper(field.Name), + Description: desc, + } + + switch fieldValue.Kind() { + case reflect.Slice: + sl, ok := defaultValue.([]string) + if !ok { + slog.Error("invalid default value", "value", defaultValue) + return + } + path.Value = strings.Join(sl, ",") + case reflect.String: + st, ok := defaultValue.(string) + if !ok { + slog.Error("invalid default value", "value", defaultValue) + return + } + // good idea to escape strings probably + if st != "" { + path.Value = fmt.Sprintf(`"%s"`, st) + } else { + path.Value = "" + } + default: + path.Value = defaultValue + } + *paths = append(*paths, path) +} + +func buildMapPaths(field reflect.StructField, parentPath string, paths *[]Path) { + fieldType := field.Type + + if fieldType.Key().Kind() != reflect.String { + slog.Info("unsupported map key type", "type", fieldType.Key().Kind()) + return + } + + mapPath := parentPath + strings.ToUpper(field.Name) + "_name_" + valueType := fieldType.Elem() + + if valueType.Kind() == reflect.Struct { + zeroValue := reflect.New(valueType).Elem() + buildPaths(valueType, zeroValue, mapPath, paths) + } +} + +func compileEnv(paths []Path) []byte { + buffer := bytes.Buffer{} + buffer.WriteString("# Tinyauth example configuration\n\n") + + for _, path := range paths { + buffer.WriteString("# ") + buffer.WriteString(path.Description) + buffer.WriteString("\n") + buffer.WriteString(path.Name) + buffer.WriteString("=") + fmt.Fprintf(&buffer, "%v", path.Value) + buffer.WriteString("\n\n") + } + + return buffer.Bytes() +} diff --git a/internal/config/config.go b/internal/config/config.go index 8b9be23..9403d62 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,5 +1,58 @@ package config +// Default configuration +func NewDefaultConfiguration() *Config { + return &Config{ + ResourcesDir: "./resources", + DatabasePath: "./tinyauth.db", + Server: ServerConfig{ + Port: 3000, + Address: "0.0.0.0", + }, + Auth: AuthConfig{ + SessionExpiry: 86400, // 1 day + SessionMaxLifetime: 0, // disabled + LoginTimeout: 300, // 5 minutes + LoginMaxRetries: 3, + }, + UI: UIConfig{ + Title: "Tinyauth", + ForgotPasswordMessage: "You can change your password by changing the configuration.", + BackgroundImage: "/background.jpg", + }, + Ldap: LdapConfig{ + Insecure: false, + SearchFilter: "(uid=%s)", + GroupCacheTTL: 900, // 15 minutes + }, + Log: LogConfig{ + Level: "info", + Json: false, + Streams: LogStreams{ + HTTP: LogStreamConfig{ + Enabled: true, + Level: "", + }, + App: LogStreamConfig{ + Enabled: true, + Level: "", + }, + Audit: LogStreamConfig{ + Enabled: false, + Level: "", + }, + }, + }, + OIDC: OIDCConfig{ + PrivateKeyPath: "./tinyauth_oidc_key", + PublicKeyPath: "./tinyauth_oidc_key.pub", + }, + Experimental: ExperimentalConfig{ + ConfigFile: "", + }, + } +} + // Version information, set at build time var Version = "development"