Compare commits

..

4 Commits

Author SHA1 Message Date
Stavros
7df60840ce fix: assign configured providers to app context 2025-12-14 19:34:45 +02:00
Stavros
2932aba750 chore: rename setup routes to setup router 2025-12-13 16:02:14 +02:00
Stavros
5cad1f0219 refactor: split bootstrap to smaller files for better readability 2025-12-13 15:58:41 +02:00
Modestas Rinkevičius
3961589f1e feat: auto-create database directory if missing (#510) 2025-12-11 14:43:57 +02:00
6 changed files with 269 additions and 195 deletions

View File

@@ -66,7 +66,7 @@ func (c *rootCmd) Register() {
{"ldap-insecure", false, "Skip certificate verification for the LDAP server."}, {"ldap-insecure", false, "Skip certificate verification for the LDAP server."},
{"ldap-search-filter", "(uid=%s)", "LDAP search filter for user lookup."}, {"ldap-search-filter", "(uid=%s)", "LDAP search filter for user lookup."},
{"resources-dir", "/data/resources", "Path to a directory containing custom resources (e.g. background image)."}, {"resources-dir", "/data/resources", "Path to a directory containing custom resources (e.g. background image)."},
{"database-path", "/data/tinyauth.db", "Path to the Sqlite database file."}, {"database-path", "/data/tinyauth.db", "Path to the Sqlite database file. Directory will be created if it doesn't exist."},
{"trusted-proxies", "", "Comma separated list of trusted proxies (IP addresses or CIDRs) for correct client IP detection."}, {"trusted-proxies", "", "Comma separated list of trusted proxies (IP addresses or CIDRs) for correct client IP detection."},
{"disable-analytics", false, "Disable anonymous version collection."}, {"disable-analytics", false, "Disable anonymous version collection."},
{"disable-resources", false, "Disable the resources server."}, {"disable-resources", false, "Disable the resources server."},

View File

@@ -13,32 +13,26 @@ import (
"time" "time"
"tinyauth/internal/config" "tinyauth/internal/config"
"tinyauth/internal/controller" "tinyauth/internal/controller"
"tinyauth/internal/middleware"
"tinyauth/internal/model" "tinyauth/internal/model"
"tinyauth/internal/service"
"tinyauth/internal/utils" "tinyauth/internal/utils"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gorm.io/gorm" "gorm.io/gorm"
) )
type Controller interface {
SetupRoutes()
}
type Middleware interface {
Middleware() gin.HandlerFunc
Init() error
}
type Service interface {
Init() error
}
type BootstrapApp struct { type BootstrapApp struct {
config config.Config config config.Config
uuid string context struct {
uuid string
cookieDomain string
sessionCookieName string
csrfCookieName string
redirectCookieName string
users []config.User
oauthProviders map[string]config.OAuthServiceConfig
configuredProviders []controller.Provider
}
services Services
} }
func NewBootstrapApp(config config.Config) *BootstrapApp { func NewBootstrapApp(config config.Config) *BootstrapApp {
@@ -55,6 +49,8 @@ func (app *BootstrapApp) Setup() error {
return err return err
} }
app.context.users = users
// Get OAuth configs // Get OAuth configs
oauthProviders, err := utils.GetOAuthProvidersConfig(os.Environ(), os.Args, app.config.AppURL) oauthProviders, err := utils.GetOAuthProvidersConfig(os.Environ(), os.Args, app.config.AppURL)
@@ -62,6 +58,8 @@ func (app *BootstrapApp) Setup() error {
return err return err
} }
app.context.oauthProviders = oauthProviders
// Get cookie domain // Get cookie domain
cookieDomain, err := utils.GetCookieDomain(app.config.AppURL) cookieDomain, err := utils.GetCookieDomain(app.config.AppURL)
@@ -69,97 +67,33 @@ func (app *BootstrapApp) Setup() error {
return err return err
} }
app.context.cookieDomain = cookieDomain
// Cookie names // Cookie names
appUrl, _ := url.Parse(app.config.AppURL) // Already validated appUrl, _ := url.Parse(app.config.AppURL) // Already validated
uuid := utils.GenerateUUID(appUrl.Hostname()) app.context.uuid = utils.GenerateUUID(appUrl.Hostname())
app.uuid = uuid cookieId := strings.Split(app.context.uuid, "-")[0]
cookieId := strings.Split(uuid, "-")[0] app.context.sessionCookieName = fmt.Sprintf("%s-%s", config.SessionCookieName, cookieId)
sessionCookieName := fmt.Sprintf("%s-%s", config.SessionCookieName, cookieId) app.context.csrfCookieName = fmt.Sprintf("%s-%s", config.CSRFCookieName, cookieId)
csrfCookieName := fmt.Sprintf("%s-%s", config.CSRFCookieName, cookieId) app.context.redirectCookieName = fmt.Sprintf("%s-%s", config.RedirectCookieName, cookieId)
redirectCookieName := fmt.Sprintf("%s-%s", config.RedirectCookieName, cookieId)
// Dumps // Dumps
log.Trace().Interface("config", app.config).Msg("Config dump") log.Trace().Interface("config", app.config).Msg("Config dump")
log.Trace().Interface("users", users).Msg("Users dump") log.Trace().Interface("users", app.context.users).Msg("Users dump")
log.Trace().Interface("oauthProviders", oauthProviders).Msg("OAuth providers dump") log.Trace().Interface("oauthProviders", app.context.oauthProviders).Msg("OAuth providers dump")
log.Trace().Str("cookieDomain", cookieDomain).Msg("Cookie domain") log.Trace().Str("cookieDomain", app.context.cookieDomain).Msg("Cookie domain")
log.Trace().Str("sessionCookieName", sessionCookieName).Msg("Session cookie name") log.Trace().Str("sessionCookieName", app.context.sessionCookieName).Msg("Session cookie name")
log.Trace().Str("csrfCookieName", csrfCookieName).Msg("CSRF cookie name") log.Trace().Str("csrfCookieName", app.context.csrfCookieName).Msg("CSRF cookie name")
log.Trace().Str("redirectCookieName", redirectCookieName).Msg("Redirect cookie name") log.Trace().Str("redirectCookieName", app.context.redirectCookieName).Msg("Redirect cookie name")
// Create configs // Services
authConfig := service.AuthServiceConfig{ services, err := app.initServices()
Users: users,
OauthWhitelist: app.config.OAuthWhitelist,
SessionExpiry: app.config.SessionExpiry,
SecureCookie: app.config.SecureCookie,
CookieDomain: cookieDomain,
LoginTimeout: app.config.LoginTimeout,
LoginMaxRetries: app.config.LoginMaxRetries,
SessionCookieName: sessionCookieName,
}
// Setup services
var ldapService *service.LdapService
if app.config.LdapAddress != "" {
ldapConfig := service.LdapServiceConfig{
Address: app.config.LdapAddress,
BindDN: app.config.LdapBindDN,
BindPassword: app.config.LdapBindPassword,
BaseDN: app.config.LdapBaseDN,
Insecure: app.config.LdapInsecure,
SearchFilter: app.config.LdapSearchFilter,
}
ldapService = service.NewLdapService(ldapConfig)
err := ldapService.Init()
if err != nil {
log.Warn().Err(err).Msg("Failed to initialize LDAP service, continuing without LDAP")
ldapService = nil
}
}
// Bootstrap database
databaseService := service.NewDatabaseService(service.DatabaseServiceConfig{
DatabasePath: app.config.DatabasePath,
})
log.Debug().Str("service", fmt.Sprintf("%T", databaseService)).Msg("Initializing service")
err = databaseService.Init()
if err != nil { if err != nil {
return fmt.Errorf("failed to initialize database service: %w", err) return fmt.Errorf("failed to initialize services: %w", err)
} }
database := databaseService.GetDatabase() app.services = services
// Create services
dockerService := service.NewDockerService()
aclsService := service.NewAccessControlsService(dockerService)
authService := service.NewAuthService(authConfig, dockerService, ldapService, database)
oauthBrokerService := service.NewOAuthBrokerService(oauthProviders)
// Initialize services (order matters)
services := []Service{
dockerService,
aclsService,
authService,
oauthBrokerService,
}
for _, svc := range services {
if svc != nil {
log.Debug().Str("service", fmt.Sprintf("%T", svc)).Msg("Initializing service")
err := svc.Init()
if err != nil {
return err
}
}
}
// Configured providers // Configured providers
configuredProviders := make([]controller.Provider, 0) configuredProviders := make([]controller.Provider, 0)
@@ -176,7 +110,7 @@ func (app *BootstrapApp) Setup() error {
return configuredProviders[i].Name < configuredProviders[j].Name return configuredProviders[i].Name < configuredProviders[j].Name
}) })
if authService.UserAuthConfigured() || ldapService != nil { if services.authService.UserAuthConfigured() {
configuredProviders = append(configuredProviders, controller.Provider{ configuredProviders = append(configuredProviders, controller.Provider{
Name: "Username", Name: "Username",
ID: "username", ID: "username",
@@ -190,92 +124,18 @@ func (app *BootstrapApp) Setup() error {
return fmt.Errorf("no authentication providers configured") return fmt.Errorf("no authentication providers configured")
} }
// Create engine app.context.configuredProviders = configuredProviders
engine := gin.New()
engine.Use(gin.Recovery())
if len(app.config.TrustedProxies) > 0 { // Setup router
err := engine.SetTrustedProxies(strings.Split(app.config.TrustedProxies, ",")) router, err := app.setupRouter()
if err != nil { if err != nil {
return fmt.Errorf("failed to set trusted proxies: %w", err) return fmt.Errorf("failed to setup routes: %w", err)
}
} }
// Create middlewares // Start DB cleanup routine
var middlewares []Middleware log.Debug().Msg("Starting database cleanup routine")
go app.dbCleanup(services.databaseService.GetDatabase())
contextMiddleware := middleware.NewContextMiddleware(middleware.ContextMiddlewareConfig{
CookieDomain: cookieDomain,
}, authService, oauthBrokerService)
uiMiddleware := middleware.NewUIMiddleware()
zerologMiddleware := middleware.NewZerologMiddleware()
middlewares = append(middlewares, contextMiddleware, uiMiddleware, zerologMiddleware)
for _, middleware := range middlewares {
log.Debug().Str("middleware", fmt.Sprintf("%T", middleware)).Msg("Initializing middleware")
err := middleware.Init()
if err != nil {
return fmt.Errorf("failed to initialize middleware %T: %w", middleware, err)
}
engine.Use(middleware.Middleware())
}
// Create routers
mainRouter := engine.Group("")
apiRouter := engine.Group("/api")
// Create controllers
contextController := controller.NewContextController(controller.ContextControllerConfig{
Providers: configuredProviders,
Title: app.config.Title,
AppURL: app.config.AppURL,
CookieDomain: cookieDomain,
ForgotPasswordMessage: app.config.ForgotPasswordMessage,
BackgroundImage: app.config.BackgroundImage,
OAuthAutoRedirect: app.config.OAuthAutoRedirect,
DisableUIWarnings: app.config.DisableUIWarnings,
}, apiRouter)
oauthController := controller.NewOAuthController(controller.OAuthControllerConfig{
AppURL: app.config.AppURL,
SecureCookie: app.config.SecureCookie,
CSRFCookieName: csrfCookieName,
RedirectCookieName: redirectCookieName,
CookieDomain: cookieDomain,
}, apiRouter, authService, oauthBrokerService)
proxyController := controller.NewProxyController(controller.ProxyControllerConfig{
AppURL: app.config.AppURL,
}, apiRouter, aclsService, authService)
userController := controller.NewUserController(controller.UserControllerConfig{
CookieDomain: cookieDomain,
}, apiRouter, authService)
resourcesController := controller.NewResourcesController(controller.ResourcesControllerConfig{
ResourcesDir: app.config.ResourcesDir,
ResourcesDisabled: app.config.DisableResources,
}, mainRouter)
healthController := controller.NewHealthController(apiRouter)
// Setup routes
controller := []Controller{
contextController,
oauthController,
proxyController,
userController,
healthController,
resourcesController,
}
for _, ctrl := range controller {
log.Debug().Msgf("Setting up %T controller", ctrl)
ctrl.SetupRoutes()
}
// If analytics are not disabled, start heartbeat // If analytics are not disabled, start heartbeat
if !app.config.DisableAnalytics { if !app.config.DisableAnalytics {
@@ -283,13 +143,8 @@ func (app *BootstrapApp) Setup() error {
go app.heartbeat() go app.heartbeat()
} }
// Start DB cleanup routine
log.Debug().Msg("Starting database cleanup routine")
go app.dbCleanup(database)
// If we have an socket path, bind to it // If we have an socket path, bind to it
if app.config.SocketPath != "" { if app.config.SocketPath != "" {
// Remove existing socket file
if _, err := os.Stat(app.config.SocketPath); err == nil { if _, err := os.Stat(app.config.SocketPath); err == nil {
log.Info().Msgf("Removing existing socket file %s", app.config.SocketPath) log.Info().Msgf("Removing existing socket file %s", app.config.SocketPath)
err := os.Remove(app.config.SocketPath) err := os.Remove(app.config.SocketPath)
@@ -298,9 +153,8 @@ func (app *BootstrapApp) Setup() error {
} }
} }
// Start server with unix socket
log.Info().Msgf("Starting server on unix socket %s", app.config.SocketPath) log.Info().Msgf("Starting server on unix socket %s", app.config.SocketPath)
if err := engine.RunUnix(app.config.SocketPath); err != nil { if err := router.RunUnix(app.config.SocketPath); err != nil {
log.Fatal().Err(err).Msg("Failed to start server") log.Fatal().Err(err).Msg("Failed to start server")
} }
@@ -310,7 +164,7 @@ func (app *BootstrapApp) Setup() error {
// Start server // Start server
address := fmt.Sprintf("%s:%d", app.config.Address, app.config.Port) address := fmt.Sprintf("%s:%d", app.config.Address, app.config.Port)
log.Info().Msgf("Starting server on %s", address) log.Info().Msgf("Starting server on %s", address)
if err := engine.Run(address); err != nil { if err := router.Run(address); err != nil {
log.Fatal().Err(err).Msg("Failed to start server") log.Fatal().Err(err).Msg("Failed to start server")
} }
@@ -328,7 +182,7 @@ func (app *BootstrapApp) heartbeat() {
var body heartbeat var body heartbeat
body.UUID = app.uuid body.UUID = app.context.uuid
body.Version = config.Version body.Version = config.Version
bodyJson, err := json.Marshal(body) bodyJson, err := json.Marshal(body)
@@ -338,7 +192,9 @@ func (app *BootstrapApp) heartbeat() {
return return
} }
client := &http.Client{} client := &http.Client{
Timeout: time.Duration(10) * time.Second, // The server should never take more than 10 seconds to respond
}
heartbeatURL := config.ApiServer + "/v1/instances/heartbeat" heartbeatURL := config.ApiServer + "/v1/instances/heartbeat"

View File

@@ -0,0 +1,105 @@
package bootstrap
import (
"fmt"
"strings"
"tinyauth/internal/controller"
"tinyauth/internal/middleware"
"github.com/gin-gonic/gin"
)
func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
engine := gin.New()
engine.Use(gin.Recovery())
if len(app.config.TrustedProxies) > 0 {
err := engine.SetTrustedProxies(strings.Split(app.config.TrustedProxies, ","))
if err != nil {
return nil, fmt.Errorf("failed to set trusted proxies: %w", err)
}
}
contextMiddleware := middleware.NewContextMiddleware(middleware.ContextMiddlewareConfig{
CookieDomain: app.context.cookieDomain,
}, app.services.authService, app.services.oauthBrokerService)
err := contextMiddleware.Init()
if err != nil {
return nil, fmt.Errorf("failed to initialize context middleware: %w", err)
}
engine.Use(contextMiddleware.Middleware())
uiMiddleware := middleware.NewUIMiddleware()
err = uiMiddleware.Init()
if err != nil {
return nil, fmt.Errorf("failed to initialize UI middleware: %w", err)
}
engine.Use(uiMiddleware.Middleware())
zerologMiddleware := middleware.NewZerologMiddleware()
err = zerologMiddleware.Init()
if err != nil {
return nil, fmt.Errorf("failed to initialize zerolog middleware: %w", err)
}
engine.Use(zerologMiddleware.Middleware())
apiRouter := engine.Group("/api")
contextController := controller.NewContextController(controller.ContextControllerConfig{
Providers: app.context.configuredProviders,
Title: app.config.Title,
AppURL: app.config.AppURL,
CookieDomain: app.context.cookieDomain,
ForgotPasswordMessage: app.config.ForgotPasswordMessage,
BackgroundImage: app.config.BackgroundImage,
OAuthAutoRedirect: app.config.OAuthAutoRedirect,
DisableUIWarnings: app.config.DisableUIWarnings,
}, apiRouter)
contextController.SetupRoutes()
oauthController := controller.NewOAuthController(controller.OAuthControllerConfig{
AppURL: app.config.AppURL,
SecureCookie: app.config.SecureCookie,
CSRFCookieName: app.context.csrfCookieName,
RedirectCookieName: app.context.redirectCookieName,
CookieDomain: app.context.cookieDomain,
}, apiRouter, app.services.authService, app.services.oauthBrokerService)
oauthController.SetupRoutes()
proxyController := controller.NewProxyController(controller.ProxyControllerConfig{
AppURL: app.config.AppURL,
}, apiRouter, app.services.accessControlService, app.services.authService)
proxyController.SetupRoutes()
userController := controller.NewUserController(controller.UserControllerConfig{
CookieDomain: app.context.cookieDomain,
}, apiRouter, app.services.authService)
userController.SetupRoutes()
resourcesController := controller.NewResourcesController(controller.ResourcesControllerConfig{
ResourcesDir: app.config.ResourcesDir,
ResourcesDisabled: app.config.DisableResources,
}, &engine.RouterGroup)
resourcesController.SetupRoutes()
healthController := controller.NewHealthController(apiRouter)
healthController.SetupRoutes()
return engine, nil
}

View File

@@ -0,0 +1,100 @@
package bootstrap
import (
"tinyauth/internal/service"
"github.com/rs/zerolog/log"
)
type Services struct {
accessControlService *service.AccessControlsService
authService *service.AuthService
databaseService *service.DatabaseService
dockerService *service.DockerService
ldapService *service.LdapService
oauthBrokerService *service.OAuthBrokerService
}
func (app *BootstrapApp) initServices() (Services, error) {
services := Services{}
databaseService := service.NewDatabaseService(service.DatabaseServiceConfig{
DatabasePath: app.config.DatabasePath,
})
err := databaseService.Init()
if err != nil {
return Services{}, err
}
services.databaseService = databaseService
ldapService := service.NewLdapService(service.LdapServiceConfig{
Address: app.config.LdapAddress,
BindDN: app.config.LdapBindDN,
BindPassword: app.config.LdapBindPassword,
BaseDN: app.config.LdapBaseDN,
Insecure: app.config.LdapInsecure,
SearchFilter: app.config.LdapSearchFilter,
})
err = ldapService.Init()
if err == nil {
services.ldapService = ldapService
} else {
log.Warn().Err(err).Msg("Failed to initialize LDAP service, continuing without it")
}
dockerService := service.NewDockerService()
err = dockerService.Init()
if err != nil {
return Services{}, err
}
services.dockerService = dockerService
accessControlsService := service.NewAccessControlsService(dockerService)
err = accessControlsService.Init()
if err != nil {
return Services{}, err
}
services.accessControlService = accessControlsService
authService := service.NewAuthService(service.AuthServiceConfig{
Users: app.context.users,
OauthWhitelist: app.config.OAuthWhitelist,
SessionExpiry: app.config.SessionExpiry,
SecureCookie: app.config.SecureCookie,
CookieDomain: app.context.cookieDomain,
LoginTimeout: app.config.LoginTimeout,
LoginMaxRetries: app.config.LoginMaxRetries,
SessionCookieName: app.context.sessionCookieName,
}, dockerService, ldapService, databaseService.GetDatabase())
err = authService.Init()
if err != nil {
return Services{}, err
}
services.authService = authService
oauthBrokerService := service.NewOAuthBrokerService(app.context.oauthProviders)
err = oauthBrokerService.Init()
if err != nil {
return Services{}, err
}
services.oauthBrokerService = oauthBrokerService
return services, nil
}

View File

@@ -37,7 +37,7 @@ type Config struct {
LdapInsecure bool `mapstructure:"ldap-insecure"` LdapInsecure bool `mapstructure:"ldap-insecure"`
LdapSearchFilter string `mapstructure:"ldap-search-filter"` LdapSearchFilter string `mapstructure:"ldap-search-filter"`
ResourcesDir string `mapstructure:"resources-dir"` ResourcesDir string `mapstructure:"resources-dir"`
DatabasePath string `mapstructure:"database-path" validate:"required"` DatabasePath string `mapstructure:"database-path"`
TrustedProxies string `mapstructure:"trusted-proxies"` TrustedProxies string `mapstructure:"trusted-proxies"`
DisableAnalytics bool `mapstructure:"disable-analytics"` DisableAnalytics bool `mapstructure:"disable-analytics"`
DisableResources bool `mapstructure:"disable-resources"` DisableResources bool `mapstructure:"disable-resources"`

View File

@@ -2,6 +2,9 @@ package service
import ( import (
"database/sql" "database/sql"
"fmt"
"os"
"path/filepath"
"tinyauth/internal/assets" "tinyauth/internal/assets"
"github.com/glebarez/sqlite" "github.com/glebarez/sqlite"
@@ -27,7 +30,17 @@ func NewDatabaseService(config DatabaseServiceConfig) *DatabaseService {
} }
func (ds *DatabaseService) Init() error { func (ds *DatabaseService) Init() error {
gormDB, err := gorm.Open(sqlite.Open(ds.config.DatabasePath), &gorm.Config{}) dbPath := ds.config.DatabasePath
if dbPath == "" {
dbPath = "/data/tinyauth.db"
}
dir := filepath.Dir(dbPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create database directory %s: %w", dir, err)
}
gormDB, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
if err != nil { if err != nil {
return err return err