diff --git a/go.mod b/go.mod index 15056c92..c39f0a92 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/stretchr/testify v1.11.1 github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298 github.com/weppos/publicsuffix-go v0.50.3 + go.uber.org/dig v1.19.0 golang.org/x/crypto v0.52.0 golang.org/x/oauth2 v0.36.0 golang.org/x/tools v0.45.0 diff --git a/go.sum b/go.sum index bbbe5c53..72917c70 100644 --- a/go.sum +++ b/go.sum @@ -485,6 +485,8 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09 go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= +go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index 7fc0cb54..50aa49ef 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -18,6 +18,7 @@ import ( "github.com/gin-gonic/gin" "github.com/steveiliop56/ding" + "go.uber.org/dig" "github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/repository" @@ -56,6 +57,7 @@ type BootstrapApp struct { db *sql.DB ding *ding.Ding listeners []Listener + dig *dig.Container } func NewBootstrapApp(config model.Config) *BootstrapApp { @@ -70,7 +72,11 @@ func (app *BootstrapApp) Setup() error { app.ctx = ctx app.cancel = cancel - // Create a ding instance + // create the dig container + c := dig.New() + app.dig = c + + // create a ding instance dg := ding.New(ctx) app.ding = dg @@ -157,12 +163,6 @@ func (app *BootstrapApp) Setup() error { app.runtime.OAuthProviders[id] = provider } - // setup oidc clients - for id, client := range app.config.OIDC.Clients { - client.ID = id - app.runtime.OIDCClients = append(app.runtime.OIDCClients, client) - } - // cookie domain cookieDomainResolver := utils.GetCookieDomain @@ -211,6 +211,33 @@ func (app *BootstrapApp) Setup() error { // store app.queries = store + // provide basic utilities to container + type utilityProvider struct { + dig.Out + + Log *logger.Logger + Config *model.Config + Runtime *model.RuntimeConfig + Ding *ding.Ding + Ctx context.Context + Queries repository.Store + } + + err = app.dig.Provide(func() utilityProvider { + return utilityProvider{ + Log: app.log, + Config: &app.config, + Runtime: &app.runtime, + Ding: app.ding, + Ctx: app.ctx, + Queries: app.queries, + } + }) + + if err != nil { + return fmt.Errorf("failed to provide utilities to container: %w", err) + } + // services err = app.setupServices() diff --git a/internal/bootstrap/router_bootstrap.go b/internal/bootstrap/router_bootstrap.go index 5244ab20..636840d6 100644 --- a/internal/bootstrap/router_bootstrap.go +++ b/internal/bootstrap/router_bootstrap.go @@ -13,6 +13,7 @@ import ( "github.com/tinyauthapp/tinyauth/internal/controller" "github.com/tinyauthapp/tinyauth/internal/middleware" "github.com/tinyauthapp/tinyauth/internal/model" + "go.uber.org/dig" "github.com/gin-gonic/gin" ) @@ -40,31 +41,94 @@ func (app *BootstrapApp) setupRouter() error { } } - contextMiddleware := middleware.NewContextMiddleware(app.log, app.runtime, app.services.authService, app.services.oauthBrokerService, app.services.tailscaleService) - engine.Use(contextMiddleware.Middleware()) + middlewareProvideFor := []any{ + middleware.NewContextMiddleware, + middleware.NewUIMiddleware, + middleware.NewZerologMiddleware, + } - uiMiddleware, err := middleware.NewUIMiddleware() + for _, provider := range middlewareProvideFor { + err := app.dig.Provide(provider) + + if err != nil { + return fmt.Errorf("failed to provide middleware: %w", err) + } + } + + type middlewareInput struct { + dig.In + + ContextMiddleware *middleware.ContextMiddleware + UIMiddleware *middleware.UIMiddleware + ZerologMiddleware *middleware.ZerologMiddleware + } + + err := app.dig.Invoke(func(mi middlewareInput) { + engine.Use(mi.ContextMiddleware.Middleware()) + engine.Use(mi.UIMiddleware.Middleware()) + engine.Use(mi.ZerologMiddleware.Middleware()) + }) if err != nil { - return fmt.Errorf("failed to initialize UI middleware: %w", err) + return fmt.Errorf("failed to invoke middleware: %w", err) } - engine.Use(uiMiddleware.Middleware()) + err = app.dig.Provide(func() *gin.RouterGroup { + return &engine.RouterGroup + }, dig.Name("mainRouterGroup")) - zerologMiddleware := middleware.NewZerologMiddleware(app.log) + if err != nil { + return fmt.Errorf("failed to provide main router group: %w", err) + } - engine.Use(zerologMiddleware.Middleware()) + err = app.dig.Provide(func() *gin.RouterGroup { + return engine.Group("/api") + }, dig.Name("apiRouterGroup")) - apiRouter := engine.Group("/api") + if err != nil { + return fmt.Errorf("failed to provide api router group: %w", err) + } - 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, &engine.RouterGroup) - controller.NewProxyController(app.log, app.runtime, apiRouter, app.services.accessControlService, app.services.authService, app.services.policyEngine) - 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) + controllerProvideFor := []any{ + controller.NewContextController, + controller.NewOAuthController, + controller.NewOIDCController, + controller.NewProxyController, + controller.NewUserController, + controller.NewResourcesController, + controller.NewHealthController, + controller.NewWellKnownController, + } + + for _, provider := range controllerProvideFor { + err := app.dig.Provide(provider) + + if err != nil { + return fmt.Errorf("failed to provide controller: %w", err) + } + } + + type controllerInput struct { + dig.In + + ContextController *controller.ContextController + OAuthController *controller.OAuthController + OIDCController *controller.OIDCController + ProxyController *controller.ProxyController + UserController *controller.UserController + ResourcesController *controller.ResourcesController + HealthController *controller.HealthController + WellKnownController *controller.WellKnownController + } + + // force dig to build all controllers and register their routes + err = app.dig.Invoke(func(ci controllerInput) error { + return nil + }) + + if err != nil { + return fmt.Errorf("failed to invoke controllers: %w", err) + } app.router = engine return nil diff --git a/internal/bootstrap/service_bootstrap.go b/internal/bootstrap/service_bootstrap.go index bf94c5c4..c9001c9e 100644 --- a/internal/bootstrap/service_bootstrap.go +++ b/internal/bootstrap/service_bootstrap.go @@ -5,54 +5,66 @@ import ( "os" "github.com/tinyauthapp/tinyauth/internal/service" + "go.uber.org/dig" ) func (app *BootstrapApp) setupServices() error { - ldapService, err := service.NewLdapService(app.log, app.config, app.ding) + err := app.setupPolicyEngine() if err != nil { - app.log.App.Warn().Err(err).Msg("Failed to initialize LDAP connection, will continue without it") + return fmt.Errorf("failed to setup policy engine: %w", err) } - app.services.ldapService = ldapService - labelProvider, err := app.getLabelProvider() if err != nil { - return fmt.Errorf("failed to initialize label provider: %w", err) + return fmt.Errorf("failed to get label provider: %w", err) } - tailscaleService, err := service.NewTailscaleService(app.log, app.config, app.ctx, app.ding) - - if err != nil { - app.log.App.Warn().Err(err).Msg("Failed to initialize Tailscale connection, will continue without it") + serviceProvideFor := []any{ + func() service.LabelProvider { + return labelProvider + }, + service.NewLdapService, + service.NewTailscaleService, + service.NewAccessControlsService, + service.NewOAuthBrokerService, + service.NewAuthService, + service.NewOIDCService, } - app.services.tailscaleService = tailscaleService - - accessControlsService := service.NewAccessControlsService(app.log, app.config, &labelProvider) - app.services.accessControlService = accessControlsService + for _, provider := range serviceProvideFor { + err = app.dig.Provide(provider) - err = app.setupPolicyEngine() - - if err != nil { - return fmt.Errorf("failed to initialize policy engine: %w", err) + if err != nil { + return fmt.Errorf("failed to provide service: %w", err) + } } - oauthBrokerService := service.NewOAuthBrokerService(app.log, app.runtime.OAuthProviders, app.ctx) - app.services.oauthBrokerService = oauthBrokerService + type svcInput struct { + dig.In - authService := service.NewAuthService(app.log, app.config, app.runtime, app.ctx, app.ding, app.services.ldapService, app.queries, app.services.oauthBrokerService, app.services.tailscaleService, app.services.policyEngine) - app.services.authService = authService + AccessControlService *service.AccessControlsService + AuthService *service.AuthService + LDAPService *service.LdapService + OAuthBrokerService *service.OAuthBrokerService + OIDCService *service.OIDCService + TailscaleService *service.TailscaleService + } - oidcService, err := service.NewOIDCService(app.log, app.config, app.runtime, app.queries, app.ding) + err = app.dig.Invoke(func(i svcInput) error { + app.services.accessControlService = i.AccessControlService + app.services.authService = i.AuthService + app.services.ldapService = i.LDAPService + app.services.oauthBrokerService = i.OAuthBrokerService + app.services.tailscaleService = i.TailscaleService + return nil + }) if err != nil { - return fmt.Errorf("failed to initialize oidc service: %w", err) + return fmt.Errorf("failed to invoke services: %w", err) } - app.services.oidcService = oidcService - return nil } @@ -69,66 +81,93 @@ func (app *BootstrapApp) getLabelProvider() (service.LabelProvider, error) { if useKubernetes { app.log.App.Debug().Msg("Using Kubernetes label provider") - kubernetesService, err := service.NewKubernetesService(app.log, app.ctx, app.ding) + err := app.dig.Provide(service.NewKubernetesService) if err != nil { - return nil, fmt.Errorf("failed to initialize kubernetes service: %w", err) + return nil, fmt.Errorf("failed to provide kubernetes service: %w", err) } - app.services.kubernetesService = kubernetesService - return kubernetesService, nil + err = app.dig.Invoke(func(k *service.KubernetesService) error { + app.services.kubernetesService = k + return nil + }) + + if err != nil { + return nil, fmt.Errorf("failed to invoke kubernetes service: %w", err) + } + + // Kubernetes will fail to initialize with an error if it cannot connect to the cluster + // but just to be safe, we check if the service is nil and log a warning if it is + if app.services.kubernetesService == nil { + if app.config.LabelProvider == "kubernetes" { + app.log.App.Warn().Msg("Kubernetes label provider selected but Kubernetes is not available, will continue without it") + } + return nil, nil + } + + return app.services.kubernetesService, nil } app.log.App.Debug().Msg("Using Docker label provider") - dockerService, err := service.NewDockerService(app.log, app.ctx, app.ding) + err := app.dig.Provide(service.NewDockerService) + + if err != nil { + return nil, fmt.Errorf("failed to provide docker service: %w", err) + } + + err = app.dig.Invoke(func(d *service.DockerService) error { + app.services.dockerService = d + return nil + }) if err != nil { - return nil, fmt.Errorf("failed to initialize docker service: %w", err) + return nil, fmt.Errorf("failed to invoke docker service: %w", err) } - if dockerService == nil { + if app.services.dockerService == nil { if app.config.LabelProvider == "docker" { app.log.App.Warn().Msg("Docker label provider selected but Docker is not available, will continue without it") } return nil, nil } - app.services.dockerService = dockerService - return dockerService, nil + return app.services.dockerService, nil default: return nil, fmt.Errorf("invalid label provider: %s", app.config.LabelProvider) } } func (app *BootstrapApp) setupPolicyEngine() error { - policyEngine, err := service.NewPolicyEngine(app.config, app.log) + err := app.dig.Provide(service.NewPolicyEngine) if err != nil { - return fmt.Errorf("failed to initialize policy engine: %w", err) + return fmt.Errorf("failed to create policy engine: %w", err) } - policyEngine.RegisterRule(service.RuleUserAllowed, &service.UserAllowedRule{ - Log: app.log, - }) - policyEngine.RegisterRule(service.RuleOAuthGroup, &service.OAuthGroupRule{ - Log: app.log, - }) - policyEngine.RegisterRule(service.RuleLDAPGroup, &service.LDAPGroupRule{ - Log: app.log, - }) - policyEngine.RegisterRule(service.RuleAuthEnabled, &service.AuthEnabledRule{ - Log: app.log, - }) - policyEngine.RegisterRule(service.RuleIPAllowed, &service.IPAllowedRule{ - Log: app.log, - Config: app.config, - }) - policyEngine.RegisterRule(service.RuleIPBypassed, &service.IPBypassedRule{ - Log: app.log, - Config: app.config, + err = app.dig.Invoke(func(policyEngine *service.PolicyEngine) error { + policyEngine.RegisterRule(service.RuleUserAllowed, &service.UserAllowedRule{ + Log: app.log, + }) + policyEngine.RegisterRule(service.RuleOAuthGroup, &service.OAuthGroupRule{ + Log: app.log, + }) + policyEngine.RegisterRule(service.RuleLDAPGroup, &service.LDAPGroupRule{ + Log: app.log, + }) + policyEngine.RegisterRule(service.RuleAuthEnabled, &service.AuthEnabledRule{ + Log: app.log, + }) + policyEngine.RegisterRule(service.RuleIPAllowed, &service.IPAllowedRule{ + Log: app.log, + Config: app.config, + }) + policyEngine.RegisterRule(service.RuleIPBypassed, &service.IPBypassedRule{ + Log: app.log, + Config: app.config, + }) + return nil }) - app.services.policyEngine = policyEngine - return nil + return err } diff --git a/internal/controller/context_controller.go b/internal/controller/context_controller.go index 487dd94d..32574c99 100644 --- a/internal/controller/context_controller.go +++ b/internal/controller/context_controller.go @@ -3,6 +3,7 @@ package controller import ( "github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/utils/logger" + "go.uber.org/dig" "github.com/gin-gonic/gin" ) @@ -71,29 +72,33 @@ type AppContextResponse struct { App ACRApp `json:"app"` } +type ContextControllerInput struct { + dig.In + + Log *logger.Logger + Config *model.Config + Runtime *model.RuntimeConfig + RouterGroup *gin.RouterGroup `name:"apiRouterGroup"` +} + type ContextController struct { log *logger.Logger - config model.Config - runtime model.RuntimeConfig + config *model.Config + runtime *model.RuntimeConfig } -func NewContextController( - log *logger.Logger, - config model.Config, - runtimeConfig model.RuntimeConfig, - router *gin.RouterGroup, -) *ContextController { +func NewContextController(i ContextControllerInput) *ContextController { controller := &ContextController{ - log: log, - config: config, - runtime: runtimeConfig, + log: i.Log, + config: i.Config, + runtime: i.Runtime, } - if !config.UI.WarningsEnabled { - log.App.Warn().Msg("UI warnings are disabled. This may lead to security issues if you are not careful. Make sure to enable warnings in production environments.") + if !i.Config.UI.WarningsEnabled { + i.Log.App.Warn().Msg("UI warnings are disabled. This may lead to security issues if you are not careful. Make sure to enable warnings in production environments.") } - contextGroup := router.Group("/context") + contextGroup := i.RouterGroup.Group("/context") contextGroup.GET("/user", controller.userContextHandler) contextGroup.GET("/app", controller.appContextHandler) diff --git a/internal/controller/context_controller_test.go b/internal/controller/context_controller_test.go index cf879645..4d729911 100644 --- a/internal/controller/context_controller_test.go +++ b/internal/controller/context_controller_test.go @@ -121,7 +121,12 @@ func TestContextController(t *testing.T) { group := router.Group("/api") gin.SetMode(gin.TestMode) - controller.NewContextController(log, cfg, runtime, group) + controller.NewContextController(controller.ContextControllerInput{ + Log: log, + Config: &cfg, + Runtime: &runtime, + RouterGroup: group, + }) recorder := httptest.NewRecorder() diff --git a/internal/controller/health_controller.go b/internal/controller/health_controller.go index 8e84e62b..2b578978 100644 --- a/internal/controller/health_controller.go +++ b/internal/controller/health_controller.go @@ -1,15 +1,24 @@ package controller -import "github.com/gin-gonic/gin" +import ( + "github.com/gin-gonic/gin" + "go.uber.org/dig" +) type HealthController struct { } -func NewHealthController(router *gin.RouterGroup) *HealthController { +type HealthControllerInput struct { + dig.In + + RouterGroup *gin.RouterGroup `name:"apiRouterGroup"` +} + +func NewHealthController(i HealthControllerInput) *HealthController { controller := &HealthController{} - router.GET("/healthz", controller.healthHandler) - router.HEAD("/healthz", controller.healthHandler) + i.RouterGroup.GET("/healthz", controller.healthHandler) + i.RouterGroup.HEAD("/healthz", controller.healthHandler) return controller } diff --git a/internal/controller/health_controller_test.go b/internal/controller/health_controller_test.go index 7576d518..9517a0d8 100644 --- a/internal/controller/health_controller_test.go +++ b/internal/controller/health_controller_test.go @@ -55,7 +55,9 @@ func TestHealthController(t *testing.T) { group := router.Group("/api") gin.SetMode(gin.TestMode) - controller.NewHealthController(group) + controller.NewHealthController(controller.HealthControllerInput{ + RouterGroup: group, + }) recorder := httptest.NewRecorder() diff --git a/internal/controller/oauth_controller.go b/internal/controller/oauth_controller.go index 788fedfa..21877705 100644 --- a/internal/controller/oauth_controller.go +++ b/internal/controller/oauth_controller.go @@ -11,6 +11,7 @@ import ( "github.com/tinyauthapp/tinyauth/internal/service" "github.com/tinyauthapp/tinyauth/internal/utils" "github.com/tinyauthapp/tinyauth/internal/utils/logger" + "go.uber.org/dig" "github.com/gin-gonic/gin" "github.com/google/go-querystring/query" @@ -22,26 +23,30 @@ type OAuthRequest struct { type OAuthController struct { log *logger.Logger - config model.Config - runtime model.RuntimeConfig + config *model.Config + runtime *model.RuntimeConfig auth *service.AuthService } -func NewOAuthController( - log *logger.Logger, - config model.Config, - runtimeConfig model.RuntimeConfig, - router *gin.RouterGroup, - auth *service.AuthService, -) *OAuthController { +type OAuthControllerInput struct { + dig.In + + Log *logger.Logger + Config *model.Config + RuntimeConfig *model.RuntimeConfig + RouterGroup *gin.RouterGroup `name:"apiRouterGroup"` + AuthService *service.AuthService +} + +func NewOAuthController(i OAuthControllerInput) *OAuthController { controller := &OAuthController{ - log: log, - config: config, - runtime: runtimeConfig, - auth: auth, + log: i.Log, + config: i.Config, + runtime: i.RuntimeConfig, + auth: i.AuthService, } - oauthGroup := router.Group("/oauth") + oauthGroup := i.RouterGroup.Group("/oauth") oauthGroup.GET("/url/:provider", controller.oauthURLHandler) oauthGroup.GET("/callback/:provider", controller.oauthCallbackHandler) diff --git a/internal/controller/oidc_controller.go b/internal/controller/oidc_controller.go index 5105f7d7..a8aef39e 100644 --- a/internal/controller/oidc_controller.go +++ b/internal/controller/oidc_controller.go @@ -11,6 +11,7 @@ import ( "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "github.com/google/go-querystring/query" + "go.uber.org/dig" "github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/service" @@ -30,7 +31,7 @@ type authorizeErrorParams struct { type OIDCController struct { log *logger.Logger oidc *service.OIDCService - runtime model.RuntimeConfig + runtime *model.RuntimeConfig } type AuthorizeCallback struct { @@ -78,22 +79,27 @@ type AuthorizeCompleteRequest struct { Ticket string `json:"ticket" binding:"required"` } -func NewOIDCController( - log *logger.Logger, - oidcService *service.OIDCService, - runtimeConfig model.RuntimeConfig, - router *gin.RouterGroup, - mainRouter *gin.RouterGroup) *OIDCController { +type OIDCControllerInput struct { + dig.In + + Log *logger.Logger + OIDCService *service.OIDCService + RuntimeConfig *model.RuntimeConfig + RouterGroup *gin.RouterGroup `name:"apiRouterGroup"` + MainRouter *gin.RouterGroup `name:"mainRouterGroup"` +} + +func NewOIDCController(i OIDCControllerInput) *OIDCController { controller := &OIDCController{ - log: log, - oidc: oidcService, - runtime: runtimeConfig, + log: i.Log, + oidc: i.OIDCService, + runtime: i.RuntimeConfig, } - mainRouter.POST("/authorize", controller.authorize) - mainRouter.GET("/authorize", controller.authorize) + i.MainRouter.POST("/authorize", controller.authorize) + i.MainRouter.GET("/authorize", controller.authorize) - oidcGroup := router.Group("/oidc") + oidcGroup := i.RouterGroup.Group("/oidc") oidcGroup.POST("/authorize-complete", controller.authorizeComplete) oidcGroup.POST("/token", controller.Token) oidcGroup.GET("/userinfo", controller.Userinfo) diff --git a/internal/controller/oidc_controller_test.go b/internal/controller/oidc_controller_test.go index d4a07baa..d8bbef17 100644 --- a/internal/controller/oidc_controller_test.go +++ b/internal/controller/oidc_controller_test.go @@ -35,7 +35,13 @@ func TestOIDCController(t *testing.T) { store := memory.New() - oidcService, err := service.NewOIDCService(log, cfg, runtime, store, dg) + oidcService, err := service.NewOIDCService(service.OIDCServiceInput{ + Log: log, + Config: &cfg, + Runtime: &runtime, + Queries: store, + Ding: dg, + }) require.NoError(t, err) // Middleware that injects an authenticated local user into the gin context, @@ -831,7 +837,13 @@ func TestOIDCController(t *testing.T) { svc = nil } - controller.NewOIDCController(log, svc, runtime, group, &router.RouterGroup) + controller.NewOIDCController(controller.OIDCControllerInput{ + Log: log, + OIDCService: svc, + RuntimeConfig: &runtime, + RouterGroup: group, + MainRouter: &router.RouterGroup, + }) recorder := httptest.NewRecorder() diff --git a/internal/controller/proxy_controller.go b/internal/controller/proxy_controller.go index 169391fc..891ff59b 100644 --- a/internal/controller/proxy_controller.go +++ b/internal/controller/proxy_controller.go @@ -13,6 +13,7 @@ import ( "github.com/tinyauthapp/tinyauth/internal/service" "github.com/tinyauthapp/tinyauth/internal/utils" "github.com/tinyauthapp/tinyauth/internal/utils/logger" + "go.uber.org/dig" "github.com/gin-gonic/gin" "github.com/google/go-querystring/query" @@ -53,29 +54,33 @@ type ProxyContext struct { type ProxyController struct { log *logger.Logger - runtime model.RuntimeConfig + runtime *model.RuntimeConfig acls *service.AccessControlsService auth *service.AuthService policyEngine *service.PolicyEngine } -func NewProxyController( - log *logger.Logger, - runtime model.RuntimeConfig, - router *gin.RouterGroup, - acls *service.AccessControlsService, - auth *service.AuthService, - policyEngine *service.PolicyEngine, -) *ProxyController { +type ProxyControllerInput struct { + dig.In + + Log *logger.Logger + RuntimeConfig *model.RuntimeConfig + RouterGroup *gin.RouterGroup `name:"apiRouterGroup"` + ACLsService *service.AccessControlsService + AuthService *service.AuthService + PolicyEngine *service.PolicyEngine +} + +func NewProxyController(i ProxyControllerInput) *ProxyController { controller := &ProxyController{ - log: log, - runtime: runtime, - acls: acls, - auth: auth, - policyEngine: policyEngine, + log: i.Log, + runtime: i.RuntimeConfig, + acls: i.ACLsService, + auth: i.AuthService, + policyEngine: i.PolicyEngine, } - proxyGroup := router.Group("/auth") + proxyGroup := i.RouterGroup.Group("/auth") proxyGroup.Any("/:proxy", controller.proxyHandler) return controller diff --git a/internal/controller/proxy_controller_test.go b/internal/controller/proxy_controller_test.go index c6a358b4..79e3e198 100644 --- a/internal/controller/proxy_controller_test.go +++ b/internal/controller/proxy_controller_test.go @@ -369,10 +369,21 @@ func TestProxyController(t *testing.T) { ctx := context.TODO() dg := ding.New(ctx) - broker := service.NewOAuthBrokerService(log, map[string]model.OAuthServiceConfig{}, ctx) - aclsService := service.NewAccessControlsService(log, cfg, nil) + broker := service.NewOAuthBrokerService(service.OAuthBrokerServiceInput{ + Log: log, + Runtime: &runtime, + Ctx: ctx, + }) + aclsService := service.NewAccessControlsService(service.AccessControlServiceInput{ + Log: log, + Config: &cfg, + LabelProvider: nil, + }) - policyEngine, err := service.NewPolicyEngine(cfg, log) + policyEngine, err := service.NewPolicyEngine(service.PolicyEngineInput{ + Log: log, + Config: &cfg, + }) require.NoError(t, err) policyEngine.RegisterRule(service.RuleUserAllowed, &service.UserAllowedRule{ @@ -395,7 +406,18 @@ func TestProxyController(t *testing.T) { Log: log, }) - authService := service.NewAuthService(log, cfg, runtime, ctx, dg, nil, store, broker, nil, policyEngine) + authService := service.NewAuthService(service.AuthServiceInput{ + Log: log, + Config: &cfg, + Runtime: &runtime, + Ctx: ctx, + Ding: dg, + LDAP: nil, + Queries: store, + OAuthBroker: broker, + Tailscale: nil, + PolicyEngine: policyEngine, + }) for _, test := range tests { t.Run(test.description, func(t *testing.T) { @@ -410,7 +432,14 @@ func TestProxyController(t *testing.T) { recorder := httptest.NewRecorder() - controller.NewProxyController(log, runtime, group, aclsService, authService, policyEngine) + controller.NewProxyController(controller.ProxyControllerInput{ + Log: log, + RuntimeConfig: &runtime, + RouterGroup: group, + ACLsService: aclsService, + AuthService: authService, + PolicyEngine: policyEngine, + }) test.run(t, router, recorder) }) diff --git a/internal/controller/resources_controller.go b/internal/controller/resources_controller.go index 1849810d..f4b720ed 100644 --- a/internal/controller/resources_controller.go +++ b/internal/controller/resources_controller.go @@ -5,25 +5,30 @@ import ( "github.com/gin-gonic/gin" "github.com/tinyauthapp/tinyauth/internal/model" + "go.uber.org/dig" ) type ResourcesController struct { - config model.Config + config *model.Config fileServer http.Handler } -func NewResourcesController( - config model.Config, - router *gin.RouterGroup, -) *ResourcesController { - fileServer := http.StripPrefix("/resources", http.FileServer(http.Dir(config.Resources.Path))) +type ResourcesControllerInput struct { + dig.In + + RouterGroup *gin.RouterGroup `name:"mainRouterGroup"` + Config *model.Config +} + +func NewResourcesController(i ResourcesControllerInput) *ResourcesController { + fileServer := http.StripPrefix("/resources", http.FileServer(http.Dir(i.Config.Resources.Path))) controller := &ResourcesController{ - config: config, + config: i.Config, fileServer: fileServer, } - router.GET("/resources/*resource", controller.resourcesHandler) + i.RouterGroup.GET("/resources/*resource", controller.resourcesHandler) return controller } diff --git a/internal/controller/resources_controller_test.go b/internal/controller/resources_controller_test.go index 68ce463d..ef225ec8 100644 --- a/internal/controller/resources_controller_test.go +++ b/internal/controller/resources_controller_test.go @@ -69,7 +69,10 @@ func TestResourcesController(t *testing.T) { group := router.Group("/") gin.SetMode(gin.TestMode) - controller.NewResourcesController(cfg, group) + controller.NewResourcesController(controller.ResourcesControllerInput{ + RouterGroup: group, + Config: &cfg, + }) recorder := httptest.NewRecorder() test.run(t, router, recorder) diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index fd3159f7..f17b7d79 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -11,6 +11,7 @@ import ( "github.com/tinyauthapp/tinyauth/internal/service" "github.com/tinyauthapp/tinyauth/internal/utils" "github.com/tinyauthapp/tinyauth/internal/utils/logger" + "go.uber.org/dig" "github.com/gin-gonic/gin" "github.com/pquerna/otp/totp" @@ -27,23 +28,27 @@ type TotpRequest struct { type UserController struct { log *logger.Logger - runtime model.RuntimeConfig + runtime *model.RuntimeConfig auth *service.AuthService } -func NewUserController( - log *logger.Logger, - runtimeConfig model.RuntimeConfig, - router *gin.RouterGroup, - auth *service.AuthService, -) *UserController { +type UserControllerInput struct { + dig.In + + Log *logger.Logger + RuntimeConfig *model.RuntimeConfig + RouterGroup *gin.RouterGroup `name:"apiRouterGroup"` + AuthService *service.AuthService +} + +func NewUserController(i UserControllerInput) *UserController { controller := &UserController{ - log: log, - runtime: runtimeConfig, - auth: auth, + log: i.Log, + runtime: i.RuntimeConfig, + auth: i.AuthService, } - userGroup := router.Group("/user") + userGroup := i.RouterGroup.Group("/user") userGroup.POST("/login", controller.loginHandler) userGroup.POST("/logout", controller.logoutHandler) userGroup.POST("/totp", controller.totpHandler) diff --git a/internal/controller/user_controller_test.go b/internal/controller/user_controller_test.go index f3c0bed2..18b8772f 100644 --- a/internal/controller/user_controller_test.go +++ b/internal/controller/user_controller_test.go @@ -414,11 +414,29 @@ func TestUserController(t *testing.T) { ctx := context.TODO() dg := ding.New(ctx) - policyEngine, err := service.NewPolicyEngine(cfg, log) + policyEngine, err := service.NewPolicyEngine(service.PolicyEngineInput{ + Log: log, + Config: &cfg, + }) require.NoError(t, err) - broker := service.NewOAuthBrokerService(log, map[string]model.OAuthServiceConfig{}, ctx) - authService := service.NewAuthService(log, cfg, runtime, ctx, dg, nil, store, broker, nil, policyEngine) + broker := service.NewOAuthBrokerService(service.OAuthBrokerServiceInput{ + Log: log, + Runtime: &runtime, + Ctx: ctx, + }) + authService := service.NewAuthService(service.AuthServiceInput{ + Log: log, + Config: &cfg, + Runtime: &runtime, + Ctx: ctx, + Ding: dg, + LDAP: nil, + Queries: store, + OAuthBroker: broker, + Tailscale: nil, + PolicyEngine: policyEngine, + }) beforeEach := func() { // Clear failed login attempts before each test @@ -437,7 +455,12 @@ func TestUserController(t *testing.T) { group := router.Group("/api") gin.SetMode(gin.TestMode) - controller.NewUserController(log, runtime, group, authService) + controller.NewUserController(controller.UserControllerInput{ + Log: log, + RuntimeConfig: &runtime, + RouterGroup: group, + AuthService: authService, + }) recorder := httptest.NewRecorder() diff --git a/internal/controller/well_known_controller.go b/internal/controller/well_known_controller.go index 8c71d890..25a40f6a 100644 --- a/internal/controller/well_known_controller.go +++ b/internal/controller/well_known_controller.go @@ -6,6 +6,7 @@ import ( "github.com/gin-gonic/gin" "github.com/tinyauthapp/tinyauth/internal/service" + "go.uber.org/dig" ) type OpenIDConnectConfiguration struct { @@ -30,13 +31,20 @@ type WellKnownController struct { oidc *service.OIDCService } -func NewWellKnownController(oidc *service.OIDCService, router *gin.RouterGroup) *WellKnownController { +type WellKnownControllerInput struct { + dig.In + + OIDCService *service.OIDCService + RouterGroup *gin.RouterGroup `name:"mainRouterGroup"` +} + +func NewWellKnownController(i WellKnownControllerInput) *WellKnownController { controller := &WellKnownController{ - oidc: oidc, + oidc: i.OIDCService, } - router.GET("/.well-known/openid-configuration", controller.OpenIDConnectConfiguration) - router.GET("/.well-known/jwks.json", controller.JWKS) + i.RouterGroup.GET("/.well-known/openid-configuration", controller.OpenIDConnectConfiguration) + i.RouterGroup.GET("/.well-known/jwks.json", controller.JWKS) return controller } diff --git a/internal/controller/well_known_controller_test.go b/internal/controller/well_known_controller_test.go index f4685723..aa421603 100644 --- a/internal/controller/well_known_controller_test.go +++ b/internal/controller/well_known_controller_test.go @@ -93,7 +93,13 @@ func TestWellKnownController(t *testing.T) { store := memory.New() - oidcService, err := service.NewOIDCService(log, cfg, runtime, store, dg) + oidcService, err := service.NewOIDCService(service.OIDCServiceInput{ + Log: log, + Config: &cfg, + Runtime: &runtime, + Queries: store, + Ding: dg, + }) require.NoError(t, err) for _, test := range tests { @@ -103,7 +109,10 @@ func TestWellKnownController(t *testing.T) { recorder := httptest.NewRecorder() - controller.NewWellKnownController(oidcService, &router.RouterGroup) + controller.NewWellKnownController(controller.WellKnownControllerInput{ + OIDCService: oidcService, + RouterGroup: &router.RouterGroup, + }) test.run(t, router, recorder) }) diff --git a/internal/middleware/context_middleware.go b/internal/middleware/context_middleware.go index fc694ddf..62c64b90 100644 --- a/internal/middleware/context_middleware.go +++ b/internal/middleware/context_middleware.go @@ -11,6 +11,7 @@ import ( "github.com/tinyauthapp/tinyauth/internal/service" "github.com/tinyauthapp/tinyauth/internal/utils" "github.com/tinyauthapp/tinyauth/internal/utils/logger" + "go.uber.org/dig" "github.com/gin-gonic/gin" ) @@ -37,25 +38,29 @@ var ( type ContextMiddleware struct { log *logger.Logger - runtime model.RuntimeConfig + runtime *model.RuntimeConfig auth *service.AuthService broker *service.OAuthBrokerService tailscale *service.TailscaleService } -func NewContextMiddleware( - log *logger.Logger, - runtime model.RuntimeConfig, - auth *service.AuthService, - broker *service.OAuthBrokerService, - tailscale *service.TailscaleService, -) *ContextMiddleware { +type ContextMiddlewareInput struct { + dig.In + + Log *logger.Logger + RuntimeConfig *model.RuntimeConfig + AuthService *service.AuthService + BrokerService *service.OAuthBrokerService + TailscaleService *service.TailscaleService +} + +func NewContextMiddleware(i ContextMiddlewareInput) *ContextMiddleware { return &ContextMiddleware{ - log: log, - runtime: runtime, - auth: auth, - broker: broker, - tailscale: tailscale, + log: i.Log, + runtime: i.RuntimeConfig, + auth: i.AuthService, + broker: i.BrokerService, + tailscale: i.TailscaleService, } } diff --git a/internal/middleware/context_middleware_test.go b/internal/middleware/context_middleware_test.go index 50ededdb..7468dec0 100644 --- a/internal/middleware/context_middleware_test.go +++ b/internal/middleware/context_middleware_test.go @@ -254,13 +254,37 @@ func TestContextMiddleware(t *testing.T) { store := memory.New() - policyEngine, err := service.NewPolicyEngine(cfg, log) + policyEngine, err := service.NewPolicyEngine(service.PolicyEngineInput{ + Log: log, + Config: &cfg, + }) require.NoError(t, err) - broker := service.NewOAuthBrokerService(log, map[string]model.OAuthServiceConfig{}, ctx) - authService := service.NewAuthService(log, cfg, runtime, ctx, dg, nil, store, broker, nil, policyEngine) - - contextMiddleware := middleware.NewContextMiddleware(log, runtime, authService, broker, nil) + broker := service.NewOAuthBrokerService(service.OAuthBrokerServiceInput{ + Log: log, + Runtime: &runtime, + Ctx: ctx, + }) + authService := service.NewAuthService(service.AuthServiceInput{ + Log: log, + Config: &cfg, + Runtime: &runtime, + Ctx: ctx, + Ding: dg, + LDAP: nil, + Queries: store, + OAuthBroker: broker, + Tailscale: nil, + PolicyEngine: policyEngine, + }) + + contextMiddleware := middleware.NewContextMiddleware(middleware.ContextMiddlewareInput{ + Log: log, + RuntimeConfig: &runtime, + AuthService: authService, + BrokerService: broker, + TailscaleService: nil, + }) for _, test := range tests { authService.ClearLoginAttempts() diff --git a/internal/middleware/ui_middleware.go b/internal/middleware/ui_middleware.go index 9f2bd297..3b706ecd 100644 --- a/internal/middleware/ui_middleware.go +++ b/internal/middleware/ui_middleware.go @@ -9,6 +9,7 @@ import ( "time" "github.com/tinyauthapp/tinyauth/internal/assets" + "go.uber.org/dig" "github.com/gin-gonic/gin" ) @@ -18,7 +19,12 @@ type UIMiddleware struct { uiFileServer http.Handler } -func NewUIMiddleware() (*UIMiddleware, error) { +// for future use if we need to inject dependencies into the middleware +type UIMiddlewareInput struct { + dig.In +} + +func NewUIMiddleware(_ UIMiddlewareInput) (*UIMiddleware, error) { m := &UIMiddleware{} ui, err := fs.Sub(assets.FrontendAssets, "dist") diff --git a/internal/middleware/zerolog_middleware.go b/internal/middleware/zerolog_middleware.go index 9870a70a..9822c2aa 100644 --- a/internal/middleware/zerolog_middleware.go +++ b/internal/middleware/zerolog_middleware.go @@ -6,6 +6,7 @@ import ( "github.com/gin-gonic/gin" "github.com/tinyauthapp/tinyauth/internal/utils/logger" + "go.uber.org/dig" ) // See context middleware for explanation of why we have to do this @@ -21,9 +22,15 @@ type ZerologMiddleware struct { log *logger.Logger } -func NewZerologMiddleware(log *logger.Logger) *ZerologMiddleware { +type ZerologMiddlewareInput struct { + dig.In + + Log *logger.Logger +} + +func NewZerologMiddleware(i ZerologMiddlewareInput) *ZerologMiddleware { return &ZerologMiddleware{ - log: log, + log: i.Log, } } diff --git a/internal/model/runtime.go b/internal/model/runtime.go index 9df20b85..0df99901 100644 --- a/internal/model/runtime.go +++ b/internal/model/runtime.go @@ -12,7 +12,6 @@ type RuntimeConfig struct { OAuthProviders map[string]OAuthServiceConfig OAuthWhitelist []string ConfiguredProviders []Provider - OIDCClients []OIDCClientConfig TrustedDomains []string } diff --git a/internal/service/access_controls_service.go b/internal/service/access_controls_service.go index 64c4d6fc..3615cce1 100644 --- a/internal/service/access_controls_service.go +++ b/internal/service/access_controls_service.go @@ -5,6 +5,7 @@ import ( "github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/utils/logger" + "go.uber.org/dig" ) type LabelProvider interface { @@ -13,19 +14,24 @@ type LabelProvider interface { type AccessControlsService struct { log *logger.Logger - config model.Config - labelProvider *LabelProvider + config *model.Config + labelProvider LabelProvider } -func NewAccessControlsService( - log *logger.Logger, - config model.Config, - labelProvider *LabelProvider) *AccessControlsService { +type AccessControlServiceInput struct { + dig.In + + Log *logger.Logger + Config *model.Config + LabelProvider LabelProvider `optional:"true"` +} + +func NewAccessControlsService(i AccessControlServiceInput) *AccessControlsService { return &AccessControlsService{ - log: log, - config: config, - labelProvider: labelProvider, + log: i.Log, + config: i.Config, + labelProvider: i.LabelProvider, } } @@ -57,8 +63,8 @@ func (service *AccessControlsService) GetAccessControls(domain string) (*model.A } // If we have a label provider configured, try to get ACLs from it - if service.labelProvider != nil && *service.labelProvider != nil { - return (*service.labelProvider).GetLabels(domain) + if service.labelProvider != nil { + return service.labelProvider.GetLabels(domain) } // no labels diff --git a/internal/service/access_controls_service_test.go b/internal/service/access_controls_service_test.go index e3d32eb6..f4f4d24c 100644 --- a/internal/service/access_controls_service_test.go +++ b/internal/service/access_controls_service_test.go @@ -87,7 +87,11 @@ func TestLookupStaticACLs(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - svc := NewAccessControlsService(log, model.Config{Apps: tt.apps}, nil) + svc := NewAccessControlsService(AccessControlServiceInput{ + Log: log, + Config: &model.Config{Apps: tt.apps}, + LabelProvider: nil, + }) got := svc.lookupStaticACLs(tt.domain) if tt.expectNil { assert.Nil(t, got) @@ -112,7 +116,11 @@ func TestGetAccessControls(t *testing.T) { }, }, } - svc := NewAccessControlsService(log, config, nil) + svc := NewAccessControlsService(AccessControlServiceInput{ + Log: log, + Config: &config, + LabelProvider: nil, + }) got, err := svc.GetAccessControls("foo.example.com") @@ -123,7 +131,11 @@ func TestGetAccessControls(t *testing.T) { }) t.Run("returns nil when no static match and no label provider", func(t *testing.T) { - svc := NewAccessControlsService(log, model.Config{}, nil) + svc := NewAccessControlsService(AccessControlServiceInput{ + Log: log, + Config: &model.Config{}, + LabelProvider: nil, + }) got, err := svc.GetAccessControls("unknown.example.com") @@ -133,7 +145,11 @@ func TestGetAccessControls(t *testing.T) { t.Run("returns nil when label provider pointer wraps a nil interface", func(t *testing.T) { var provider LabelProvider - svc := NewAccessControlsService(log, model.Config{}, &provider) + svc := NewAccessControlsService(AccessControlServiceInput{ + Log: log, + Config: &model.Config{}, + LabelProvider: provider, // nil provider + }) got, err := svc.GetAccessControls("unknown.example.com") @@ -152,7 +168,11 @@ func TestGetAccessControls(t *testing.T) { }, } var provider LabelProvider = mock - svc := NewAccessControlsService(log, model.Config{}, &provider) + svc := NewAccessControlsService(AccessControlServiceInput{ + Log: log, + Config: &model.Config{}, + LabelProvider: provider, + }) got, err := svc.GetAccessControls("dynamic.example.com") @@ -170,7 +190,11 @@ func TestGetAccessControls(t *testing.T) { "foo": {Config: model.AppConfig{Domain: "foo.example.com"}}, }, } - svc := NewAccessControlsService(log, config, &provider) + svc := NewAccessControlsService(AccessControlServiceInput{ + Log: log, + Config: &config, + LabelProvider: provider, + }) got, err := svc.GetAccessControls("foo.example.com") @@ -188,7 +212,11 @@ func TestGetAccessControls(t *testing.T) { }, } var provider LabelProvider = mock - svc := NewAccessControlsService(log, model.Config{}, &provider) + svc := NewAccessControlsService(AccessControlServiceInput{ + Log: log, + Config: &model.Config{}, + LabelProvider: provider, + }) got, err := svc.GetAccessControls("dynamic.example.com") diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index ef3e9e08..4e6da9b4 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -14,6 +14,7 @@ import ( "github.com/tinyauthapp/tinyauth/internal/repository" "github.com/tinyauthapp/tinyauth/internal/utils" "github.com/tinyauthapp/tinyauth/internal/utils/logger" + "go.uber.org/dig" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" @@ -57,8 +58,8 @@ type LoginAttempt struct { type AuthService struct { log *logger.Logger - config model.Config - runtime model.RuntimeConfig + config *model.Config + runtime *model.RuntimeConfig ctx context.Context ldap *LdapService @@ -82,28 +83,32 @@ type AuthService struct { } } -func NewAuthService( - log *logger.Logger, - config model.Config, - runtime model.RuntimeConfig, - ctx context.Context, - dg *ding.Ding, - ldap *LdapService, - queries repository.Store, - oauthBroker *OAuthBrokerService, - tailscale *TailscaleService, - policy *PolicyEngine, -) *AuthService { +type AuthServiceInput struct { + dig.In + + Log *logger.Logger + Config *model.Config + Runtime *model.RuntimeConfig + Ctx context.Context + Ding *ding.Ding + LDAP *LdapService `optional:"true"` + Queries repository.Store + OAuthBroker *OAuthBrokerService + Tailscale *TailscaleService `optional:"true"` + PolicyEngine *PolicyEngine +} + +func NewAuthService(i AuthServiceInput) *AuthService { service := &AuthService{ - log: log, - runtime: runtime, - ctx: ctx, - config: config, - ldap: ldap, - queries: queries, - oauthBroker: oauthBroker, - tailscale: tailscale, - policyEngine: policy, + log: i.Log, + runtime: i.Runtime, + ctx: i.Ctx, + config: i.Config, + ldap: i.LDAP, + queries: i.Queries, + oauthBroker: i.OAuthBroker, + tailscale: i.Tailscale, + policyEngine: i.PolicyEngine, } // caches setup @@ -115,7 +120,7 @@ func NewAuthService( service.caches.login = loginCache service.caches.ldap = ldapCache - dg.Go(func(ctx context.Context) { + i.Ding.Go(func(ctx context.Context) { ticker := time.NewTicker(1 * time.Minute) defer ticker.Stop() diff --git a/internal/service/auth_service_test.go b/internal/service/auth_service_test.go index 3000adcc..d0752721 100644 --- a/internal/service/auth_service_test.go +++ b/internal/service/auth_service_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/utils/logger" ) @@ -12,9 +13,22 @@ func TestIsEmailWhitelistedUsesProviderSpecificList(t *testing.T) { log := logger.NewLogger().WithTestConfig() log.Init() + policyEngine, err := NewPolicyEngine(PolicyEngineInput{ + Log: log, + Config: &model.Config{ + Auth: model.AuthConfig{ + ACLs: model.ACLsConfig{ + Policy: string(PolicyAllow), + }, + }, + }, + }) + + require.NoError(t, err) + auth := &AuthService{ log: log, - runtime: model.RuntimeConfig{ + runtime: &model.RuntimeConfig{ OAuthWhitelist: []string{"global@example.com"}, OAuthProviders: map[string]model.OAuthServiceConfig{ "github": { @@ -28,6 +42,7 @@ func TestIsEmailWhitelistedUsesProviderSpecificList(t *testing.T) { }, }, }, + policyEngine: policyEngine, } assert.True(t, auth.IsEmailWhitelisted("github", "github@example.com")) diff --git a/internal/service/docker_service.go b/internal/service/docker_service.go index 6525b7f7..49708b0d 100644 --- a/internal/service/docker_service.go +++ b/internal/service/docker_service.go @@ -8,6 +8,7 @@ import ( "github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/utils/decoders" "github.com/tinyauthapp/tinyauth/internal/utils/logger" + "go.uber.org/dig" container "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" @@ -21,36 +22,40 @@ type DockerService struct { isConnected bool } -func NewDockerService( - log *logger.Logger, - ctx context.Context, - dg *ding.Ding, -) (*DockerService, error) { +type DockerServiceInput struct { + dig.In + + Log *logger.Logger + Ctx context.Context + Ding *ding.Ding +} + +func NewDockerService(i DockerServiceInput) (*DockerService, error) { client, err := client.NewClientWithOpts(client.FromEnv) if err != nil { return nil, err } - client.NegotiateAPIVersion(ctx) + client.NegotiateAPIVersion(i.Ctx) - _, err = client.Ping(ctx) + _, err = client.Ping(i.Ctx) if err != nil { - log.App.Debug().Err(err).Msg("Docker not connected") + i.Log.App.Debug().Err(err).Msg("Docker not connected") return nil, nil } service := &DockerService{ - log: log, + log: i.Log, client: client, - context: ctx, + context: i.Ctx, } service.isConnected = true service.log.App.Debug().Msg("Docker connected successfully") - dg.Go(service.watchAndClose, ding.RingMajor) + i.Ding.Go(service.watchAndClose, ding.RingMajor) return service, nil } diff --git a/internal/service/kubernetes_service.go b/internal/service/kubernetes_service.go index 9cef6759..f065be72 100644 --- a/internal/service/kubernetes_service.go +++ b/internal/service/kubernetes_service.go @@ -12,6 +12,7 @@ import ( "github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/utils/decoders" "github.com/tinyauthapp/tinyauth/internal/utils/logger" + "go.uber.org/dig" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -48,11 +49,15 @@ type KubernetesService struct { appNameIndex map[string]ingressAppKey } -func NewKubernetesService( - log *logger.Logger, - ctx context.Context, - dg *ding.Ding, -) (*KubernetesService, error) { +type KubernetesServiceInput struct { + dig.In + + Log *logger.Logger + Ctx context.Context + Ding *ding.Ding +} + +func NewKubernetesService(i KubernetesServiceInput) (*KubernetesService, error) { cfg, err := rest.InClusterConfig() if err != nil { return nil, fmt.Errorf("failed to get in-cluster kubernetes config: %w", err) @@ -69,31 +74,31 @@ func NewKubernetesService( Resource: "ingresses", } - accessCtx, accessCancel := context.WithTimeout(ctx, 5*time.Second) + accessCtx, accessCancel := context.WithTimeout(i.Ctx, 5*time.Second) defer accessCancel() _, err = client.Resource(gvr).List(accessCtx, metav1.ListOptions{Limit: 1}) if err != nil { - log.App.Warn().Err(err).Str("api", gvr.GroupVersion().String()).Msg("Failed to access Ingress API, Kubernetes label provider will be disabled") + i.Log.App.Warn().Err(err).Str("api", gvr.GroupVersion().String()).Msg("Failed to access Ingress API, Kubernetes label provider will be disabled") return nil, fmt.Errorf("failed to access ingress api: %w", err) } - log.App.Debug().Str("api", gvr.GroupVersion().String()).Msg("Successfully accessed Ingress API, starting watcher") + i.Log.App.Debug().Str("api", gvr.GroupVersion().String()).Msg("Successfully accessed Ingress API, starting watcher") service := &KubernetesService{ - log: log, + log: i.Log, client: client, ingressApps: make(map[ingressKey][]ingressApp), domainIndex: make(map[string]ingressAppKey), appNameIndex: make(map[string]ingressAppKey), } - dg.Go(func(ctx context.Context) { + i.Ding.Go(func(ctx context.Context) { service.watchGVR(gvr, ctx) }, ding.RingMajor) service.started = true - log.App.Debug().Msg("Kubernetes label provider started successfully") + i.Log.App.Debug().Msg("Kubernetes label provider started successfully") return service, nil } diff --git a/internal/service/ldap_service.go b/internal/service/ldap_service.go index 819cb9d3..66bb57b4 100644 --- a/internal/service/ldap_service.go +++ b/internal/service/ldap_service.go @@ -13,44 +13,48 @@ import ( "github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/utils" "github.com/tinyauthapp/tinyauth/internal/utils/logger" + "go.uber.org/dig" ) type LdapService struct { log *logger.Logger - config model.Config + config *model.Config - conn *ldapgo.Conn - mutex sync.RWMutex - cert *tls.Certificate + conn *ldapgo.Conn + mutex sync.RWMutex + cert *tls.Certificate + bindPw string } -func NewLdapService( - log *logger.Logger, - config model.Config, - dg *ding.Ding, -) (*LdapService, error) { - if config.LDAP.Address == "" { +type LdapServiceInput struct { + dig.In + + Log *logger.Logger + Config *model.Config + Ding *ding.Ding +} + +func NewLdapService(i LdapServiceInput) (*LdapService, error) { + if i.Config.LDAP.Address == "" { return nil, nil } - secret := utils.GetSecret(config.LDAP.BindPassword, config.LDAP.BindPasswordFile) - config.LDAP.BindPassword = secret - config.LDAP.BindPasswordFile = "" - ldap := &LdapService{ - log: log, - config: config, + log: i.Log, + config: i.Config, } + ldap.bindPw = utils.GetSecret(i.Config.LDAP.BindPassword, i.Config.LDAP.BindPasswordFile) + // Check whether authentication with client certificate is possible - if config.LDAP.AuthCert != "" && config.LDAP.AuthKey != "" { - cert, err := tls.LoadX509KeyPair(config.LDAP.AuthCert, config.LDAP.AuthKey) + if i.Config.LDAP.AuthCert != "" && i.Config.LDAP.AuthKey != "" { + cert, err := tls.LoadX509KeyPair(i.Config.LDAP.AuthCert, i.Config.LDAP.AuthKey) if err != nil { return nil, fmt.Errorf("failed to initialize LDAP with mTLS authentication: %w", err) } - log.App.Info().Msg("LDAP mTLS authentication configured successfully") + i.Log.App.Info().Msg("LDAP mTLS authentication configured successfully") ldap.cert = &cert @@ -72,7 +76,7 @@ func NewLdapService( return nil, fmt.Errorf("failed to connect to ldap server: %w", err) } - dg.Go(func(ctx context.Context) { + i.Ding.Go(func(ctx context.Context) { ldap.log.App.Debug().Msg("Starting LDAP connection heartbeat routine") ticker := time.NewTicker(5 * time.Minute) @@ -217,7 +221,7 @@ func (ldap *LdapService) BindService(rebind bool) error { if ldap.cert != nil { return ldap.conn.ExternalBind() } - return ldap.conn.Bind(ldap.config.LDAP.BindDN, ldap.config.LDAP.BindPassword) + return ldap.conn.Bind(ldap.config.LDAP.BindDN, ldap.bindPw) } func (ldap *LdapService) Bind(userDN string, password string) error { diff --git a/internal/service/oauth_broker_service.go b/internal/service/oauth_broker_service.go index fdb5e1e0..63503abc 100644 --- a/internal/service/oauth_broker_service.go +++ b/internal/service/oauth_broker_service.go @@ -5,6 +5,7 @@ import ( "github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/utils/logger" + "go.uber.org/dig" "slices" @@ -32,23 +33,27 @@ var presets = map[string]func(config model.OAuthServiceConfig, ctx context.Conte "google": newGoogleOAuthService, } -func NewOAuthBrokerService( - log *logger.Logger, - configs map[string]model.OAuthServiceConfig, - ctx context.Context, -) *OAuthBrokerService { +type OAuthBrokerServiceInput struct { + dig.In + + Log *logger.Logger + Runtime *model.RuntimeConfig + Ctx context.Context +} + +func NewOAuthBrokerService(i OAuthBrokerServiceInput) *OAuthBrokerService { service := &OAuthBrokerService{ - log: log, + log: i.Log, services: make(map[string]OAuthServiceImpl), - configs: configs, + configs: i.Runtime.OAuthProviders, } - for name, cfg := range configs { + for name, cfg := range service.configs { if presetFunc, exists := presets[name]; exists { - service.services[name] = presetFunc(cfg, ctx) + service.services[name] = presetFunc(cfg, i.Ctx) service.log.App.Debug().Str("service", name).Msg("Loaded OAuth service from preset") } else { - service.services[name] = NewOAuthService(cfg, name, ctx) + service.services[name] = NewOAuthService(cfg, name, i.Ctx) service.log.App.Debug().Str("service", name).Msg("Loaded OAuth service from custom config") } } diff --git a/internal/service/oidc_service.go b/internal/service/oidc_service.go index 3fec6f48..da988c49 100644 --- a/internal/service/oidc_service.go +++ b/internal/service/oidc_service.go @@ -14,6 +14,7 @@ import ( "fmt" "net/url" "os" + "path/filepath" "strings" "time" @@ -26,6 +27,7 @@ import ( "github.com/tinyauthapp/tinyauth/internal/repository" "github.com/tinyauthapp/tinyauth/internal/utils" "github.com/tinyauthapp/tinyauth/internal/utils/logger" + "go.uber.org/dig" ) var ( @@ -133,8 +135,8 @@ type UsedCodeEntry struct { type OIDCService struct { log *logger.Logger - config model.Config - runtime model.RuntimeConfig + config *model.Config + runtime *model.RuntimeConfig queries repository.Store clients map[string]model.OIDCClientConfig @@ -149,19 +151,24 @@ type OIDCService struct { } } -func NewOIDCService( - log *logger.Logger, - config model.Config, - runtime model.RuntimeConfig, - queries repository.Store, - dg *ding.Ding) (*OIDCService, error) { +type OIDCServiceInput struct { + dig.In + + Log *logger.Logger + Config *model.Config + Runtime *model.RuntimeConfig + Queries repository.Store + Ding *ding.Ding +} + +func NewOIDCService(i OIDCServiceInput) (*OIDCService, error) { // If not configured, skip init - if len(runtime.OIDCClients) == 0 { + if len(i.Config.OIDC.Clients) == 0 { return nil, nil } // Ensure issuer is https - uissuer, err := url.Parse(runtime.AppURL) + uissuer, err := url.Parse(i.Runtime.AppURL) if err != nil { return nil, fmt.Errorf("failed to parse app url: %w", err) @@ -174,14 +181,14 @@ func NewOIDCService( issuer := fmt.Sprintf("%s://%s", uissuer.Scheme, uissuer.Host) // Create/load private and public keys - if strings.TrimSpace(config.OIDC.PrivateKeyPath) == "" || - strings.TrimSpace(config.OIDC.PublicKeyPath) == "" { + if strings.TrimSpace(i.Config.OIDC.PrivateKeyPath) == "" || + strings.TrimSpace(i.Config.OIDC.PublicKeyPath) == "" { return nil, errors.New("private key path and public key path are required") } var privateKey *rsa.PrivateKey - fprivateKey, err := os.ReadFile(config.OIDC.PrivateKeyPath) + fprivateKey, err := os.ReadFile(i.Config.OIDC.PrivateKeyPath) if err != nil && !errors.Is(err, os.ErrNotExist) { return nil, err @@ -200,8 +207,12 @@ func NewOIDCService( Type: "RSA PRIVATE KEY", Bytes: der, }) - log.App.Trace().Str("type", "RSA PRIVATE KEY").Msg("Generated private RSA key") - err = os.WriteFile(config.OIDC.PrivateKeyPath, encoded, 0600) + i.Log.App.Trace().Str("type", "RSA PRIVATE KEY").Msg("Generated private RSA key") + err := os.MkdirAll(filepath.Dir(i.Config.OIDC.PrivateKeyPath), 0700) + if err != nil { + return nil, fmt.Errorf("failed to create directory for private key: %w", err) + } + err = os.WriteFile(i.Config.OIDC.PrivateKeyPath, encoded, 0600) if err != nil { return nil, fmt.Errorf("failed to write private key to file: %w", err) } @@ -210,7 +221,7 @@ func NewOIDCService( if block == nil { return nil, errors.New("failed to decode private key") } - log.App.Trace().Str("type", block.Type).Msg("Loaded private key") + i.Log.App.Trace().Str("type", block.Type).Msg("Loaded private key") privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) if err != nil { return nil, fmt.Errorf("failed to parse private key: %w", err) @@ -219,7 +230,7 @@ func NewOIDCService( var publicKey crypto.PublicKey - fpublicKey, err := os.ReadFile(config.OIDC.PublicKeyPath) + fpublicKey, err := os.ReadFile(i.Config.OIDC.PublicKeyPath) if err != nil && !errors.Is(err, os.ErrNotExist) { return nil, fmt.Errorf("failed to read public key: %w", err) @@ -235,8 +246,12 @@ func NewOIDCService( Type: "RSA PUBLIC KEY", Bytes: der, }) - log.App.Trace().Str("type", "RSA PUBLIC KEY").Msg("Generated public RSA key") - err = os.WriteFile(config.OIDC.PublicKeyPath, encoded, 0644) + i.Log.App.Trace().Str("type", "RSA PUBLIC KEY").Msg("Generated public RSA key") + err := os.MkdirAll(filepath.Dir(i.Config.OIDC.PublicKeyPath), 0700) + if err != nil { + return nil, fmt.Errorf("failed to create directory for public key: %w", err) + } + err = os.WriteFile(i.Config.OIDC.PublicKeyPath, encoded, 0644) if err != nil { return nil, err } @@ -245,7 +260,7 @@ func NewOIDCService( if block == nil { return nil, errors.New("failed to decode public key") } - log.App.Trace().Str("type", block.Type).Msg("Loaded public key") + i.Log.App.Trace().Str("type", block.Type).Msg("Loaded public key") switch block.Type { case "RSA PUBLIC KEY": publicKey, err = x509.ParsePKCS1PublicKey(block.Bytes) @@ -275,7 +290,7 @@ func NewOIDCService( // We will reorganize the client into a map with the client ID as the key clients := make(map[string]model.OIDCClientConfig) - for id, client := range config.OIDC.Clients { + for id, client := range i.Config.OIDC.Clients { client.ID = id if client.Name == "" { client.Name = utils.Capitalize(client.ID) @@ -291,15 +306,15 @@ func NewOIDCService( } client.ClientSecretFile = "" clients[id] = client - log.App.Debug().Str("clientId", client.ClientID).Msg("Loaded OIDC client configuration") + i.Log.App.Debug().Str("clientId", client.ClientID).Msg("Loaded OIDC client configuration") } // Initialize the service service := &OIDCService{ - log: log, - config: config, - runtime: runtime, - queries: queries, + log: i.Log, + config: i.Config, + runtime: i.Runtime, + queries: i.Queries, clients: clients, privateKey: privateKey, @@ -308,7 +323,7 @@ func NewOIDCService( } // Start cleanup routine - dg.Go(service.cleanupRoutine, ding.RingMinor) + i.Ding.Go(service.cleanupRoutine, ding.RingMinor) // Create caches codeCash := NewCacheStore[AuthorizeCodeEntry](256) @@ -320,7 +335,7 @@ func NewOIDCService( service.caches.authorize = authorize // Start cache cleanup routine - dg.Go(func(ctx context.Context) { + i.Ding.Go(func(ctx context.Context) { ticker := time.NewTicker(1 * time.Minute) defer ticker.Stop() diff --git a/internal/service/oidc_service_test.go b/internal/service/oidc_service_test.go index 48078a9d..5197d7c7 100644 --- a/internal/service/oidc_service_test.go +++ b/internal/service/oidc_service_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/require" "github.com/tinyauthapp/tinyauth/internal/model" + "github.com/tinyauthapp/tinyauth/internal/repository/memory" "github.com/tinyauthapp/tinyauth/internal/service" "github.com/tinyauthapp/tinyauth/internal/utils/logger" ) @@ -67,7 +68,15 @@ func TestCompileUserinfo(t *testing.T) { ctx := context.TODO() dg := ding.New(ctx) - svc, err := service.NewOIDCService(log, cfg, runtime, nil, dg) + store := memory.New() + + svc, err := service.NewOIDCService(service.OIDCServiceInput{ + Log: log, + Config: &cfg, + Runtime: &runtime, + Queries: store, + Ding: dg, + }) require.NoError(t, err) type testCase struct { diff --git a/internal/service/policy_engine.go b/internal/service/policy_engine.go index 7f301da6..c3bbb133 100644 --- a/internal/service/policy_engine.go +++ b/internal/service/policy_engine.go @@ -6,6 +6,7 @@ import ( "github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/utils/logger" + "go.uber.org/dig" ) type Policy string @@ -40,21 +41,28 @@ type PolicyEngine struct { policy Policy } -func NewPolicyEngine(config model.Config, log *logger.Logger) (*PolicyEngine, error) { +type PolicyEngineInput struct { + dig.In + + Log *logger.Logger + Config *model.Config +} + +func NewPolicyEngine(i PolicyEngineInput) (*PolicyEngine, error) { engine := PolicyEngine{ - log: log, + log: i.Log, rules: make(map[RuleName]Rule), } - switch config.Auth.ACLs.Policy { + switch i.Config.Auth.ACLs.Policy { case string(PolicyAllow): - log.App.Debug().Msg("Using 'allow' ACL policy: access to apps will be allowed by default unless explicitly blocked") + i.Log.App.Debug().Msg("Using 'allow' ACL policy: access to apps will be allowed by default unless explicitly blocked") engine.policy = PolicyAllow case string(PolicyDeny): - log.App.Debug().Msg("Using 'deny' ACL policy: access to apps will be blocked by default unless explicitly allowed") + i.Log.App.Debug().Msg("Using 'deny' ACL policy: access to apps will be blocked by default unless explicitly allowed") engine.policy = PolicyDeny default: - return nil, fmt.Errorf("invalid acl policy: %s", config.Auth.ACLs.Policy) + return nil, fmt.Errorf("invalid acl policy: %s", i.Config.Auth.ACLs.Policy) } return &engine, nil diff --git a/internal/service/policy_engine_test.go b/internal/service/policy_engine_test.go index d1ef4796..1c6120a0 100644 --- a/internal/service/policy_engine_test.go +++ b/internal/service/policy_engine_test.go @@ -33,23 +33,35 @@ func TestPolicyEngine(t *testing.T) { // Engine should fail with invalid policy cfg.Auth.ACLs.Policy = "invalid_policy" - _, err := service.NewPolicyEngine(cfg, log) + _, err := service.NewPolicyEngine(service.PolicyEngineInput{ + Log: log, + Config: &cfg, + }) assert.Error(t, err) // Engine should initialize with 'allow' policy cfg.Auth.ACLs.Policy = string(service.PolicyAllow) - engine, err := service.NewPolicyEngine(cfg, log) + engine, err := service.NewPolicyEngine(service.PolicyEngineInput{ + Log: log, + Config: &cfg, + }) assert.NoError(t, err) assert.Equal(t, service.PolicyAllow, engine.Policy()) // Engine should initialize with 'deny' policy cfg.Auth.ACLs.Policy = string(service.PolicyDeny) - engine, err = service.NewPolicyEngine(cfg, log) + engine, err = service.NewPolicyEngine(service.PolicyEngineInput{ + Log: log, + Config: &cfg, + }) assert.NoError(t, err) assert.Equal(t, service.PolicyDeny, engine.Policy()) // Engine should allow adding rules - engine, err = service.NewPolicyEngine(cfg, log) + engine, err = service.NewPolicyEngine(service.PolicyEngineInput{ + Log: log, + Config: &cfg, + }) assert.NoError(t, err) engine.RegisterRule("test-rule", testRule) _, ok := engine.Rules()["test-rule"] @@ -57,7 +69,10 @@ func TestPolicyEngine(t *testing.T) { // Begin allow policy tests cfg.Auth.ACLs.Policy = string(service.PolicyAllow) - engine, err = service.NewPolicyEngine(cfg, log) + engine, err = service.NewPolicyEngine(service.PolicyEngineInput{ + Log: log, + Config: &cfg, + }) assert.NoError(t, err) engine.RegisterRule("test-rule", testRule) @@ -75,7 +90,10 @@ func TestPolicyEngine(t *testing.T) { // Begin deny policy tests cfg.Auth.ACLs.Policy = string(service.PolicyDeny) - engine, err = service.NewPolicyEngine(cfg, log) + engine, err = service.NewPolicyEngine(service.PolicyEngineInput{ + Log: log, + Config: &cfg, + }) assert.NoError(t, err) engine.RegisterRule("test-rule", testRule) diff --git a/internal/service/tailscale_service.go b/internal/service/tailscale_service.go index c869c671..38692385 100644 --- a/internal/service/tailscale_service.go +++ b/internal/service/tailscale_service.go @@ -12,6 +12,7 @@ import ( "github.com/steveiliop56/ding" "github.com/tinyauthapp/tinyauth/internal/model" "github.com/tinyauthapp/tinyauth/internal/utils/logger" + "go.uber.org/dig" "tailscale.com/client/local" "tailscale.com/tsnet" ) @@ -25,7 +26,7 @@ type TailscaleWhoisResponse struct { type TailscaleService struct { log *logger.Logger - config model.Config + config *model.Config ctx context.Context srv *tsnet.Server @@ -34,22 +35,31 @@ type TailscaleService struct { mu sync.Mutex } -func NewTailscaleService(log *logger.Logger, config model.Config, ctx context.Context, dg *ding.Ding) (*TailscaleService, error) { - if !config.Tailscale.Enabled { +type TailscaleServiceInput struct { + dig.In + + Log *logger.Logger + Config *model.Config + Ctx context.Context + Ding *ding.Ding +} + +func NewTailscaleService(i TailscaleServiceInput) (*TailscaleService, error) { + if !i.Config.Tailscale.Enabled { return nil, nil } srv := new(tsnet.Server) // node options - srv.Dir = config.Tailscale.Dir - srv.Hostname = config.Tailscale.Hostname - srv.AuthKey = config.Tailscale.AuthKey - srv.Ephemeral = config.Tailscale.Ephemeral + srv.Dir = i.Config.Tailscale.Dir + srv.Hostname = i.Config.Tailscale.Hostname + srv.AuthKey = i.Config.Tailscale.AuthKey + srv.Ephemeral = i.Config.Tailscale.Ephemeral // redirect logs to zerolog - srv.Logf = log.App.Printf - srv.UserLogf = log.App.Printf + srv.Logf = i.Log.App.Printf + srv.UserLogf = i.Log.App.Printf err := srv.Start() @@ -65,14 +75,14 @@ func NewTailscaleService(log *logger.Logger, config model.Config, ctx context.Co } service := &TailscaleService{ - log: log, - config: config, - ctx: ctx, + log: i.Log, + config: i.Config, + ctx: i.Ctx, srv: srv, lc: lc, } - connectCtx, cancel := context.WithTimeout(ctx, 2*time.Minute) // large enough timeout to allow for user to manually authenticate with link if needed + connectCtx, cancel := context.WithTimeout(i.Ctx, 2*time.Minute) // large enough timeout to allow for user to manually authenticate with link if needed defer cancel() err = service.waitForConn(connectCtx) @@ -82,7 +92,7 @@ func NewTailscaleService(log *logger.Logger, config model.Config, ctx context.Co return nil, fmt.Errorf("failed to connect to tailscale network: %w", err) } - dg.Go(service.watchAndClose, ding.RingMajor) + i.Ding.Go(service.watchAndClose, ding.RingMajor) return service, nil } diff --git a/internal/test/test.go b/internal/test/test.go index 415591fa..76c31a27 100644 --- a/internal/test/test.go +++ b/internal/test/test.go @@ -121,14 +121,6 @@ func CreateTestConfigs(t *testing.T) (model.Config, model.RuntimeConfig) { CookieDomain: "example.com", AppURL: "https://tinyauth.example.com", SessionCookieName: "tinyauth-session", - OIDCClients: func() []model.OIDCClientConfig { - var clients []model.OIDCClientConfig - for id, client := range config.OIDC.Clients { - client.ID = id - clients = append(clients, client) - } - return clients - }(), } return config, runtime