Merge branch 'main' into feat/tailscale

This commit is contained in:
Stavros
2026-05-10 16:44:39 +03:00
81 changed files with 4474 additions and 2655 deletions
+279 -159
View File
@@ -3,157 +3,195 @@ package bootstrap
import (
"bytes"
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"os"
"os/signal"
"sort"
"strings"
"sync"
"syscall"
"time"
"github.com/gin-gonic/gin"
"github.com/tinyauthapp/tinyauth/internal/config"
"github.com/tinyauthapp/tinyauth/internal/controller"
"github.com/tinyauthapp/tinyauth/internal/model"
"github.com/tinyauthapp/tinyauth/internal/repository"
"github.com/tinyauthapp/tinyauth/internal/service"
"github.com/tinyauthapp/tinyauth/internal/utils"
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
)
type BootstrapApp struct {
config config.Config
context struct {
appUrl string
uuid string
cookieDomain string
sessionCookieName string
csrfCookieName string
redirectCookieName string
oauthSessionCookieName string
users []config.User
oauthProviders map[string]config.OAuthServiceConfig
configuredProviders []controller.Provider
oidcClients []config.OIDCClientConfig
}
services Services
type Services struct {
accessControlService *service.AccessControlsService
authService *service.AuthService
dockerService *service.DockerService
kubernetesService *service.KubernetesService
ldapService *service.LdapService
oauthBrokerService *service.OAuthBrokerService
oidcService *service.OIDCService
}
func NewBootstrapApp(config config.Config) *BootstrapApp {
type BootstrapApp struct {
config model.Config
runtime model.RuntimeConfig
services Services
log *logger.Logger
ctx context.Context
cancel context.CancelFunc
queries *repository.Queries
router *gin.Engine
db *sql.DB
wg sync.WaitGroup
}
func NewBootstrapApp(config model.Config) *BootstrapApp {
return &BootstrapApp{
config: config,
}
}
func (app *BootstrapApp) Setup() error {
// create context
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
app.ctx = ctx
app.cancel = cancel
// setup logger
log := logger.NewLogger().WithConfig(app.config.Log)
log.Init()
app.log = log
// get app url
if app.config.AppURL == "" {
return fmt.Errorf("app URL cannot be empty, perhaps config loading failed")
return errors.New("app url cannot be empty, perhaps config loading failed")
}
appUrl, err := url.Parse(app.config.AppURL)
if err != nil {
return err
return fmt.Errorf("failed to parse app url: %w", err)
}
app.context.appUrl = appUrl.Scheme + "://" + appUrl.Host
app.runtime.AppURL = appUrl.Scheme + "://" + appUrl.Host
// validate session config
if app.config.Auth.SessionMaxLifetime != 0 && app.config.Auth.SessionMaxLifetime < app.config.Auth.SessionExpiry {
return fmt.Errorf("session max lifetime cannot be less than session expiry")
return errors.New("session max lifetime cannot be less than session expiry")
}
// Parse users
// parse users
users, err := utils.GetUsers(app.config.Auth.Users, app.config.Auth.UsersFile, app.config.Auth.UserAttributes)
if err != nil {
return err
return fmt.Errorf("failed to load users: %w", err)
}
app.context.users = users
app.runtime.LocalUsers = *users
// Setup OAuth providers
app.context.oauthProviders = app.config.OAuth.Providers
// load oauth whitelist
oauthWhitelist, err := utils.GetStringList(app.config.OAuth.Whitelist, app.config.OAuth.WhitelistFile)
for name, provider := range app.context.oauthProviders {
if err != nil {
return fmt.Errorf("failed to load oauth whitelist: %w", err)
}
app.runtime.OAuthWhitelist = oauthWhitelist
// setup oauth providers
app.runtime.OAuthProviders = app.config.OAuth.Providers
for id, provider := range app.runtime.OAuthProviders {
secret := utils.GetSecret(provider.ClientSecret, provider.ClientSecretFile)
provider.ClientSecret = secret
provider.ClientSecretFile = ""
if provider.RedirectURL == "" {
provider.RedirectURL = app.context.appUrl + "/api/oauth/callback/" + name
provider.RedirectURL = app.runtime.AppURL + "/api/oauth/callback/" + id
}
app.context.oauthProviders[name] = provider
app.runtime.OAuthProviders[id] = provider
}
for id, provider := range app.context.oauthProviders {
// set presets for built-in providers
for id, provider := range app.runtime.OAuthProviders {
if provider.Name == "" {
if name, ok := config.OverrideProviders[id]; ok {
if name, ok := model.OverrideProviders[id]; ok {
provider.Name = name
} else {
provider.Name = utils.Capitalize(id)
}
}
app.context.oauthProviders[id] = provider
app.runtime.OAuthProviders[id] = provider
}
// Setup OIDC clients
// setup oidc clients
for id, client := range app.config.OIDC.Clients {
client.ID = id
app.context.oidcClients = append(app.context.oidcClients, client)
app.runtime.OIDCClients = append(app.runtime.OIDCClients, client)
}
// Get cookie domain
cookieDomain, err := utils.GetCookieDomain(app.context.appUrl)
// cookie domain
cookieDomainResolver := utils.GetCookieDomain
if !app.config.Auth.SubdomainsEnabled {
app.log.App.Warn().Msg("Subdomains are disabled, using standalone cookie domain resolver which will not work with subdomains")
cookieDomainResolver = utils.GetStandaloneCookieDomain
}
cookieDomain, err := cookieDomainResolver(app.runtime.AppURL)
if err != nil {
return err
return fmt.Errorf("failed to get cookie domain: %w", err)
}
app.context.cookieDomain = cookieDomain
app.runtime.CookieDomain = cookieDomain
// Cookie names
app.context.uuid = utils.GenerateUUID(appUrl.Hostname())
cookieId := strings.Split(app.context.uuid, "-")[0]
app.context.sessionCookieName = fmt.Sprintf("%s-%s", config.SessionCookieName, cookieId)
app.context.csrfCookieName = fmt.Sprintf("%s-%s", config.CSRFCookieName, cookieId)
app.context.redirectCookieName = fmt.Sprintf("%s-%s", config.RedirectCookieName, cookieId)
app.context.oauthSessionCookieName = fmt.Sprintf("%s-%s", config.OAuthSessionCookieName, cookieId)
// cookie names
app.runtime.UUID = utils.GenerateUUID(appUrl.Hostname())
// Dumps
tlog.App.Trace().Interface("config", app.config).Msg("Config dump")
tlog.App.Trace().Interface("users", app.context.users).Msg("Users dump")
tlog.App.Trace().Interface("oauthProviders", app.context.oauthProviders).Msg("OAuth providers dump")
tlog.App.Trace().Str("cookieDomain", app.context.cookieDomain).Msg("Cookie domain")
tlog.App.Trace().Str("sessionCookieName", app.context.sessionCookieName).Msg("Session cookie name")
tlog.App.Trace().Str("csrfCookieName", app.context.csrfCookieName).Msg("CSRF cookie name")
tlog.App.Trace().Str("redirectCookieName", app.context.redirectCookieName).Msg("Redirect cookie name")
cookieId := strings.Split(app.runtime.UUID, "-")[0] // first 8 characters of the uuid should be good enough
// Database
db, err := app.SetupDatabase(app.config.Database.Path)
app.runtime.SessionCookieName = fmt.Sprintf("%s-%s", model.SessionCookieName, cookieId)
app.runtime.CSRFCookieName = fmt.Sprintf("%s-%s", model.CSRFCookieName, cookieId)
app.runtime.RedirectCookieName = fmt.Sprintf("%s-%s", model.RedirectCookieName, cookieId)
app.runtime.OAuthSessionCookieName = fmt.Sprintf("%s-%s", model.OAuthSessionCookieName, cookieId)
// database
err = app.SetupDatabase()
if err != nil {
return fmt.Errorf("failed to setup database: %w", err)
}
// Queries
queries := repository.New(db)
// after this point, we start initializing dependencies so it's a good time to setup a defer
// to ensure that resources are cleaned up properly in case of an error during initialization
defer func() {
app.cancel()
app.wg.Wait()
app.db.Close()
}()
// Services
services, err := app.initServices(queries)
// queries
queries := repository.New(app.db)
app.queries = queries
// services
err = app.setupServices()
if err != nil {
return fmt.Errorf("failed to initialize services: %w", err)
}
app.services = services
// configured providers
configuredProviders := make([]model.Provider, 0)
// Configured providers
configuredProviders := make([]controller.Provider, 0)
for id, provider := range app.context.oauthProviders {
configuredProviders = append(configuredProviders, controller.Provider{
for id, provider := range app.runtime.OAuthProviders {
configuredProviders = append(configuredProviders, model.Provider{
Name: provider.Name,
ID: id,
OAuth: true,
@@ -164,106 +202,171 @@ func (app *BootstrapApp) Setup() error {
return configuredProviders[i].Name < configuredProviders[j].Name
})
if services.authService.LocalAuthConfigured() {
configuredProviders = append(configuredProviders, controller.Provider{
if app.services.authService.LocalAuthConfigured() {
configuredProviders = append(configuredProviders, model.Provider{
Name: "Local",
ID: "local",
OAuth: false,
})
}
if services.authService.LdapAuthConfigured() {
configuredProviders = append(configuredProviders, controller.Provider{
if app.services.authService.LDAPAuthConfigured() {
configuredProviders = append(configuredProviders, model.Provider{
Name: "LDAP",
ID: "ldap",
OAuth: false,
})
}
tlog.App.Debug().Interface("providers", configuredProviders).Msg("Authentication providers")
if len(configuredProviders) == 0 {
return fmt.Errorf("no authentication providers configured")
return errors.New("no authentication providers configured")
}
app.context.configuredProviders = configuredProviders
for _, provider := range configuredProviders {
app.log.App.Debug().Str("provider", provider.Name).Msg("Configured authentication provider")
}
// Setup router
router, err := app.setupRouter()
app.runtime.ConfiguredProviders = configuredProviders
// setup router
err = app.setupRouter()
if err != nil {
return fmt.Errorf("failed to setup routes: %w", err)
}
// Start db cleanup routine
tlog.App.Debug().Msg("Starting database cleanup routine")
go app.dbCleanupRoutine(queries)
// start db cleanup routine
app.log.App.Debug().Msg("Starting database cleanup routine")
app.wg.Go(app.dbCleanupRoutine)
// If analytics are not disabled, start heartbeat
// if analytics are not disabled, start heartbeat
if app.config.Analytics.Enabled {
tlog.App.Debug().Msg("Starting heartbeat routine")
go app.heartbeatRoutine()
app.log.App.Debug().Msg("Starting heartbeat routine")
app.wg.Go(app.heartbeatRoutine)
}
// Start listeners and monitor for errors
err = app.setupListeners(router)
// create err channel to listen for server errors
errChanLen := 0
if err != nil {
return fmt.Errorf("server error: %w", err)
runUnix := app.config.Server.SocketPath != ""
runHTTP := app.config.Server.SocketPath == "" || app.config.Server.ConcurrentListenersEnabled
if runUnix {
errChanLen++
}
if runHTTP {
errChanLen++
}
errChan := make(chan error, errChanLen)
if app.config.Server.ConcurrentListenersEnabled {
app.log.App.Info().Msg("Concurrent listeners enabled, will run on all available listeners")
}
// serve unix
if runUnix {
app.wg.Go(func() {
if err := app.serveUnix(); err != nil {
errChan <- err
}
})
}
// serve to http
if runHTTP {
app.wg.Go(func() {
if err := app.serveHTTP(); err != nil {
errChan <- err
}
})
}
// monitor cancellation and server errors
for {
select {
case <-app.ctx.Done():
app.log.App.Info().Msg("Oh, it's time for me to go, bye!")
return nil
case err := <-errChan:
if err != nil {
return fmt.Errorf("server error: %w", err)
}
}
}
}
func (app *BootstrapApp) serveHTTP() error {
address := fmt.Sprintf("%s:%d", app.config.Server.Address, app.config.Server.Port)
app.log.App.Info().Msgf("Starting server on %s", address)
server := &http.Server{
Addr: address,
Handler: app.router.Handler(),
}
go func() {
<-app.ctx.Done()
app.log.App.Debug().Msg("Shutting down http listener")
server.Shutdown(app.ctx)
}()
err := server.ListenAndServe()
if err != nil && !errors.Is(err, http.ErrServerClosed) {
return fmt.Errorf("failed to start http listener: %w", err)
}
return nil
}
func (app *BootstrapApp) setupListeners(router *gin.Engine) error {
errChan := make(chan error, 1)
// First check socket
if app.config.Server.SocketPath != "" {
if _, err := os.Stat(app.config.Server.SocketPath); err == nil {
tlog.App.Info().Msgf("Removing existing socket file %s", app.config.Server.SocketPath)
err := os.Remove(app.config.Server.SocketPath)
if err != nil {
return fmt.Errorf("failed to remove existing socket file: %w", err)
}
}
tlog.App.Info().Msgf("Starting server on unix socket %s", app.config.Server.SocketPath)
go func() {
err := router.RunUnix(app.config.Server.SocketPath)
if err != nil {
errChan <- fmt.Errorf("failed to start server on unix socket: %w", err)
}
}()
func (app *BootstrapApp) serveUnix() error {
if app.config.Server.SocketPath == "" {
return nil
}
// Then normal TCP listener
address := fmt.Sprintf("%s:%d", app.config.Server.Address, app.config.Server.Port)
tlog.App.Info().Msgf("Starting server on %s", address)
_, err := os.Stat(app.config.Server.SocketPath)
if err == nil {
app.log.App.Info().Msgf("Removing existing socket file %s", app.config.Server.SocketPath)
err := os.Remove(app.config.Server.SocketPath)
if err != nil {
return fmt.Errorf("failed to remove existing socket file: %w", err)
}
}
app.log.App.Info().Msgf("Starting server on unix socket %s", app.config.Server.SocketPath)
listener, err := net.Listen("unix", app.config.Server.SocketPath)
if err != nil {
return fmt.Errorf("failed to create unix socket listener: %w", err)
}
server := &http.Server{
Handler: app.router.Handler(),
}
shutdown := func() {
server.Shutdown(app.ctx)
listener.Close()
os.Remove(app.config.Server.SocketPath)
}
go func() {
err := router.Run(address)
if err != nil {
errChan <- fmt.Errorf("failed to start server on TCP: %w", err)
}
<-app.ctx.Done()
app.log.App.Debug().Msg("Shutting down unix socket listener")
shutdown()
}()
// Finally tailscale listener if configured
if app.services.tailscaleService.IsConnfigured() {
tailscaleListener, err := app.services.tailscaleService.CreateListener()
if err != nil {
return fmt.Errorf("failed to create tailscale listener: %w", err)
}
err = server.Serve(listener)
tlog.App.Info().Msgf("Starting server on Tailscale interface with hostname %s", app.services.tailscaleService.GetHostname())
go func() {
err := router.RunListener(tailscaleListener)
if err != nil {
errChan <- fmt.Errorf("failed to start server on Tailscale interface: %w", err)
}
}()
if err != nil && !errors.Is(err, http.ErrServerClosed) {
shutdown()
return fmt.Errorf("failed to start unix socket listener: %w", err)
}
return <-errChan
@@ -273,20 +376,20 @@ func (app *BootstrapApp) heartbeatRoutine() {
ticker := time.NewTicker(time.Duration(12) * time.Hour)
defer ticker.Stop()
type heartbeat struct {
type Heartbeat struct {
UUID string `json:"uuid"`
Version string `json:"version"`
}
var body heartbeat
var body Heartbeat
body.UUID = app.context.uuid
body.Version = config.Version
body.UUID = app.runtime.UUID
body.Version = model.Version
bodyJson, err := json.Marshal(body)
if err != nil {
tlog.App.Error().Err(err).Msg("Failed to marshal heartbeat body")
app.log.App.Error().Err(err).Msg("Failed to marshal heartbeat body, heartbeat routine will not start")
return
}
@@ -294,45 +397,62 @@ func (app *BootstrapApp) heartbeatRoutine() {
Timeout: 30 * time.Second, // The server should never take more than 30 seconds to respond
}
heartbeatURL := config.ApiServer + "/v1/instances/heartbeat"
heartbeatURL := model.APIServer + "/v1/instances/heartbeat"
for range ticker.C {
tlog.App.Debug().Msg("Sending heartbeat")
for {
select {
case <-ticker.C:
app.log.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 {
tlog.App.Error().Err(err).Msg("Failed to create heartbeat request")
continue
}
if err != nil {
app.log.App.Error().Err(err).Msg("Failed to create heartbeat request")
continue
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
res, err := client.Do(req)
if err != nil {
tlog.App.Error().Err(err).Msg("Failed to send heartbeat")
continue
}
if err != nil {
app.log.App.Error().Err(err).Msg("Failed to send heartbeat")
continue
}
res.Body.Close()
res.Body.Close()
if res.StatusCode != 200 && res.StatusCode != 201 {
tlog.App.Debug().Str("status", res.Status).Msg("Heartbeat returned non-200/201 status")
if res.StatusCode != 200 && res.StatusCode != 201 {
app.log.App.Debug().Str("status", res.Status).Msg("Heartbeat returned non-200/201 status")
}
case <-app.ctx.Done():
app.log.App.Debug().Msg("Stopping heartbeat routine")
ticker.Stop()
return
}
}
}
func (app *BootstrapApp) dbCleanupRoutine(queries *repository.Queries) {
func (app *BootstrapApp) dbCleanupRoutine() {
ticker := time.NewTicker(time.Duration(30) * time.Minute)
defer ticker.Stop()
ctx := context.Background()
for range ticker.C {
tlog.App.Debug().Msg("Cleaning up old database sessions")
err := queries.DeleteExpiredSessions(ctx, time.Now().Unix())
if err != nil {
tlog.App.Error().Err(err).Msg("Failed to clean up old database sessions")
for {
select {
case <-ticker.C:
app.log.App.Debug().Msg("Running database cleanup")
err := app.queries.DeleteExpiredSessions(app.ctx, time.Now().Unix())
if err != nil {
app.log.App.Error().Err(err).Msg("Failed to delete expired sessions")
}
app.log.App.Debug().Msg("Database cleanup completed")
case <-app.ctx.Done():
app.log.App.Debug().Msg("Stopping database cleanup routine")
ticker.Stop()
return
}
}
}
+22 -10
View File
@@ -14,19 +14,26 @@ import (
_ "modernc.org/sqlite"
)
func (app *BootstrapApp) SetupDatabase(databasePath string) (*sql.DB, error) {
dir := filepath.Dir(databasePath)
func (app *BootstrapApp) SetupDatabase() error {
dir := filepath.Dir(app.config.Database.Path)
if err := os.MkdirAll(dir, 0750); err != nil {
return nil, fmt.Errorf("failed to create database directory %s: %w", dir, err)
return fmt.Errorf("failed to create database directory %s: %w", dir, err)
}
db, err := sql.Open("sqlite", databasePath)
db, err := sql.Open("sqlite", app.config.Database.Path)
if err != nil {
return nil, fmt.Errorf("failed to open database: %w", err)
return fmt.Errorf("failed to open database: %w", err)
}
// Close the database if there is an error during migration
defer func() {
if err != nil {
db.Close()
}
}()
// Limit to 1 connection to sequence writes, this may need to be revisited in the future
// if the sqlite connection starts being a bottleneck
db.SetMaxOpenConns(1)
@@ -34,24 +41,29 @@ func (app *BootstrapApp) SetupDatabase(databasePath string) (*sql.DB, error) {
migrations, err := iofs.New(assets.Migrations, "migrations")
if err != nil {
return nil, fmt.Errorf("failed to create migrations: %w", err)
return fmt.Errorf("failed to create migrations: %w", err)
}
target, err := sqlite3.WithInstance(db, &sqlite3.Config{})
if err != nil {
return nil, fmt.Errorf("failed to create sqlite3 instance: %w", err)
return fmt.Errorf("failed to create sqlite3 instance: %w", err)
}
migrator, err := migrate.NewWithInstance("iofs", migrations, "sqlite3", target)
if err != nil {
return nil, fmt.Errorf("failed to create migrator: %w", err)
return fmt.Errorf("failed to create migrator: %w", err)
}
if err := migrator.Up(); err != nil && err != migrate.ErrNoChange {
return nil, fmt.Errorf("failed to migrate database: %w", err)
return fmt.Errorf("failed to migrate database: %w", err)
}
return db, nil
app.db = db
return nil
}
func (app *BootstrapApp) GetDB() *sql.DB {
return app.db
}
+18 -85
View File
@@ -2,21 +2,16 @@ package bootstrap
import (
"fmt"
"slices"
"github.com/tinyauthapp/tinyauth/internal/config"
"github.com/tinyauthapp/tinyauth/internal/controller"
"github.com/tinyauthapp/tinyauth/internal/middleware"
"github.com/gin-gonic/gin"
)
var DEV_MODES = []string{"main", "test", "development"}
func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
if !slices.Contains(DEV_MODES, config.Version) {
gin.SetMode(gin.ReleaseMode)
}
func (app *BootstrapApp) setupRouter() error {
// we don't want gin debug mode
gin.SetMode(gin.ReleaseMode)
engine := gin.New()
engine.Use(gin.Recovery())
@@ -25,98 +20,36 @@ func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
err := engine.SetTrustedProxies(app.config.Auth.TrustedProxies)
if err != nil {
return nil, fmt.Errorf("failed to set trusted proxies: %w", err)
return fmt.Errorf("failed to set trusted proxies: %w", err)
}
}
contextMiddleware := middleware.NewContextMiddleware(middleware.ContextMiddlewareConfig{
CookieDomain: app.context.cookieDomain,
}, app.services.authService, app.services.oauthBrokerService, app.services.tailscaleService)
err := contextMiddleware.Init()
if err != nil {
return nil, fmt.Errorf("failed to initialize context middleware: %w", err)
}
contextMiddleware := middleware.NewContextMiddleware(app.log, app.runtime, app.services.authService, app.services.oauthBrokerService)
engine.Use(contextMiddleware.Middleware())
uiMiddleware := middleware.NewUIMiddleware()
err = uiMiddleware.Init()
uiMiddleware, err := middleware.NewUIMiddleware()
if err != nil {
return nil, fmt.Errorf("failed to initialize UI middleware: %w", err)
return 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)
}
zerologMiddleware := middleware.NewZerologMiddleware(app.log)
engine.Use(zerologMiddleware.Middleware())
apiRouter := engine.Group("/api")
contextController := controller.NewContextController(controller.ContextControllerConfig{
Providers: app.context.configuredProviders,
Title: app.config.UI.Title,
AppURL: app.config.AppURL,
CookieDomain: app.context.cookieDomain,
ForgotPasswordMessage: app.config.UI.ForgotPasswordMessage,
BackgroundImage: app.config.UI.BackgroundImage,
OAuthAutoRedirect: app.config.OAuth.AutoRedirect,
WarningsEnabled: app.config.UI.WarningsEnabled,
}, apiRouter)
controller.NewContextController(app.log, app.config, app.runtime, apiRouter)
controller.NewOAuthController(app.log, app.config, app.runtime, apiRouter, app.services.authService)
controller.NewOIDCController(app.log, app.services.oidcService, app.runtime, apiRouter)
controller.NewProxyController(app.log, app.runtime, apiRouter, app.services.accessControlService, app.services.authService)
controller.NewUserController(app.log, app.runtime, apiRouter, app.services.authService)
controller.NewResourcesController(app.config, &engine.RouterGroup)
controller.NewHealthController(apiRouter)
controller.NewWellKnownController(app.services.oidcService, &engine.RouterGroup)
contextController.SetupRoutes()
oauthController := controller.NewOAuthController(controller.OAuthControllerConfig{
AppURL: app.config.AppURL,
SecureCookie: app.config.Auth.SecureCookie,
CSRFCookieName: app.context.csrfCookieName,
RedirectCookieName: app.context.redirectCookieName,
CookieDomain: app.context.cookieDomain,
OAuthSessionCookieName: app.context.oauthSessionCookieName,
}, apiRouter, app.services.authService)
oauthController.SetupRoutes()
oidcController := controller.NewOIDCController(controller.OIDCControllerConfig{}, app.services.oidcService, apiRouter)
oidcController.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{
Path: app.config.Resources.Path,
Enabled: app.config.Resources.Enabled,
}, &engine.RouterGroup)
resourcesController.SetupRoutes()
healthController := controller.NewHealthController(apiRouter)
healthController.SetupRoutes()
wellknownController := controller.NewWellKnownController(controller.WellKnownControllerConfig{}, app.services.oidcService, engine)
wellknownController.SetupRoutes()
return engine, nil
app.router = engine
return nil
}
+39 -106
View File
@@ -3,132 +3,65 @@ package bootstrap
import (
"fmt"
"github.com/tinyauthapp/tinyauth/internal/repository"
"os"
"github.com/tinyauthapp/tinyauth/internal/service"
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
)
type Services struct {
accessControlService *service.AccessControlsService
authService *service.AuthService
dockerService *service.DockerService
ldapService *service.LdapService
oauthBrokerService *service.OAuthBrokerService
oidcService *service.OIDCService
tailscaleService *service.TailscaleService
}
func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, error) {
services := Services{}
ldapService := service.NewLdapService(service.LdapServiceConfig{
Address: app.config.Ldap.Address,
BindDN: app.config.Ldap.BindDN,
BindPassword: app.config.Ldap.BindPassword,
BaseDN: app.config.Ldap.BaseDN,
Insecure: app.config.Ldap.Insecure,
SearchFilter: app.config.Ldap.SearchFilter,
AuthCert: app.config.Ldap.AuthCert,
AuthKey: app.config.Ldap.AuthKey,
})
err := ldapService.Init()
func (app *BootstrapApp) setupServices() error {
ldapService, err := service.NewLdapService(app.log, app.config, app.ctx, &app.wg)
if err != nil {
tlog.App.Warn().Err(err).Msg("Failed to setup LDAP service, starting without it")
ldapService.Unconfigure()
app.log.App.Warn().Err(err).Msg("Failed to initialize LDAP connection, will continue without it")
}
services.ldapService = ldapService
app.services.ldapService = ldapService
dockerService := service.NewDockerService()
useKubernetes := app.config.LabelProvider == "kubernetes" ||
(app.config.LabelProvider == "auto" && os.Getenv("KUBERNETES_SERVICE_HOST") != "")
err = dockerService.Init()
var labelProvider service.LabelProvider
if err != nil {
return Services{}, err
}
if useKubernetes {
app.log.App.Debug().Msg("Using Kubernetes label provider")
services.dockerService = dockerService
kubernetesService, err := service.NewKubernetesService(app.log, app.ctx, &app.wg)
accessControlsService := service.NewAccessControlsService(dockerService, app.config.Apps)
if err != nil {
return fmt.Errorf("failed to initialize kubernetes service: %w", err)
}
err = accessControlsService.Init()
if err != nil {
return Services{}, err
}
services.accessControlService = accessControlsService
oauthBrokerService := service.NewOAuthBrokerService(app.context.oauthProviders)
err = oauthBrokerService.Init()
if err != nil {
return Services{}, err
}
services.oauthBrokerService = oauthBrokerService
tailscaleHostname := app.config.Tailscale.Hostname
if tailscaleHostname == "" {
tailscaleHostname = fmt.Sprintf("tinyauth-%s", app.context.uuid)
}
tailscaleService := service.NewTailscaleService(service.TailscaleServiceConfig{
Dir: app.config.Tailscale.Dir,
Hostname: tailscaleHostname,
AuthKey: app.config.Tailscale.AuthKey,
})
err = tailscaleService.Init()
if err != nil {
tlog.App.Warn().Err(err).Msg("Failed to setup Tailscale service, starting without it")
tailscaleService.Destroy()
app.services.kubernetesService = kubernetesService
labelProvider = kubernetesService
} else {
services.tailscaleService = tailscaleService
app.log.App.Debug().Msg("Using Docker label provider")
dockerService, err := service.NewDockerService(app.log, app.ctx, &app.wg)
if err != nil {
return fmt.Errorf("failed to initialize docker service: %w", err)
}
app.services.dockerService = dockerService
labelProvider = dockerService
}
authService := service.NewAuthService(service.AuthServiceConfig{
Users: app.context.users,
OauthWhitelist: app.config.OAuth.Whitelist,
SessionExpiry: app.config.Auth.SessionExpiry,
SessionMaxLifetime: app.config.Auth.SessionMaxLifetime,
SecureCookie: app.config.Auth.SecureCookie,
CookieDomain: app.context.cookieDomain,
LoginTimeout: app.config.Auth.LoginTimeout,
LoginMaxRetries: app.config.Auth.LoginMaxRetries,
SessionCookieName: app.context.sessionCookieName,
IP: app.config.Auth.IP,
LDAPGroupsCacheTTL: app.config.Ldap.GroupCacheTTL,
}, dockerService, services.ldapService, queries, services.oauthBrokerService)
accessControlsService := service.NewAccessControlsService(app.log, &labelProvider, app.config.Apps)
app.services.accessControlService = accessControlsService
err = authService.Init()
oauthBrokerService := service.NewOAuthBrokerService(app.log, app.runtime.OAuthProviders, app.ctx)
app.services.oauthBrokerService = oauthBrokerService
authService := service.NewAuthService(app.log, app.config, app.runtime, app.ctx, &app.wg, app.services.ldapService, app.queries, app.services.oauthBrokerService)
app.services.authService = authService
oidcService, err := service.NewOIDCService(app.log, app.config, app.runtime, app.queries, app.ctx, &app.wg)
if err != nil {
return Services{}, err
return fmt.Errorf("failed to initialize oidc service: %w", err)
}
services.authService = authService
app.services.oidcService = oidcService
oidcService := service.NewOIDCService(service.OIDCServiceConfig{
Clients: app.config.OIDC.Clients,
PrivateKeyPath: app.config.OIDC.PrivateKeyPath,
PublicKeyPath: app.config.OIDC.PublicKeyPath,
Issuer: app.config.AppURL,
SessionExpiry: app.config.Auth.SessionExpiry,
}, queries)
err = oidcService.Init()
if err != nil {
return Services{}, err
}
services.oidcService = oidcService
return services, nil
return nil
}