From 15a375362220309bf69200868a228f64a094f9c9 Mon Sep 17 00:00:00 2001 From: Stavros Date: Sun, 29 Mar 2026 19:18:46 +0300 Subject: [PATCH] test: add proxy controller tests --- internal/controller/proxy_controller_test.go | 625 ++++++++++--------- 1 file changed, 347 insertions(+), 278 deletions(-) diff --git a/internal/controller/proxy_controller_test.go b/internal/controller/proxy_controller_test.go index f7e73ec..e23f585 100644 --- a/internal/controller/proxy_controller_test.go +++ b/internal/controller/proxy_controller_test.go @@ -1,302 +1,371 @@ package controller_test import ( - "net/http" "net/http/httptest" + "os" "testing" + "github.com/gin-gonic/gin" "github.com/steveiliop56/tinyauth/internal/bootstrap" "github.com/steveiliop56/tinyauth/internal/config" "github.com/steveiliop56/tinyauth/internal/controller" "github.com/steveiliop56/tinyauth/internal/repository" "github.com/steveiliop56/tinyauth/internal/service" - - "github.com/gin-gonic/gin" - "gotest.tools/v3/assert" + "github.com/steveiliop56/tinyauth/internal/utils/tlog" + "github.com/stretchr/testify/assert" ) -var loggedInCtx = config.UserContext{ - Username: "test", - Name: "Test", - Email: "test@example.com", - IsLoggedIn: true, - Provider: "local", -} - -func setupProxyController(t *testing.T, middlewares []gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder) { - // Setup - gin.SetMode(gin.TestMode) - router := gin.Default() - - if len(middlewares) > 0 { - for _, m := range middlewares { - router.Use(m) - } - } - - group := router.Group("/api") - recorder := httptest.NewRecorder() - - // Mock app - app := bootstrap.NewBootstrapApp(config.Config{}) - - // Database - db, err := app.SetupDatabase(":memory:") - - assert.NilError(t, err) - - // Queries - queries := repository.New(db) - - // Docker - dockerService := service.NewDockerService() - - assert.NilError(t, dockerService.Init()) - - // Access controls - accessControlsService := service.NewAccessControlsService(dockerService, map[string]config.App{ - "whoami": { - Path: config.AppPath{ - Allow: "/allow", - }, - }, - }) - - assert.NilError(t, accessControlsService.Init()) - - // Auth service - authService := service.NewAuthService(service.AuthServiceConfig{ +func TestProxyController(t *testing.T) { + authServiceCfg := service.AuthServiceConfig{ Users: []config.User{ { Username: "testuser", - Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.", // test + Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", }, { Username: "totpuser", - Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.", - TotpSecret: "foo", + Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", + TotpSecret: "JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK", }, }, - OauthWhitelist: []string{}, - SessionExpiry: 3600, - SessionMaxLifetime: 0, - SecureCookie: false, - CookieDomain: "localhost", - LoginTimeout: 300, - LoginMaxRetries: 3, - SessionCookieName: "tinyauth-session", - }, dockerService, nil, queries, &service.OAuthBrokerService{}) + SessionExpiry: 10, // 10 seconds, useful for testing + CookieDomain: "example.com", + LoginTimeout: 10, // 10 seconds, useful for testing + LoginMaxRetries: 3, + SessionCookieName: "tinyauth-session", + } - // Controller - ctrl := controller.NewProxyController(controller.ProxyControllerConfig{ - AppURL: "http://tinyauth.example.com", - }, group, accessControlsService, authService) - ctrl.SetupRoutes() + controllerCfg := controller.ProxyControllerConfig{ + AppURL: "https://tinyauth.example.com", + } - return router, recorder -} - -// TODO: Needs tests for context middleware - -func TestProxyHandler(t *testing.T) { - // Test logged out user traefik/caddy (forward_auth) - router, recorder := setupProxyController(t, nil) - - req, err := http.NewRequest("GET", "/api/auth/traefik", nil) - assert.NilError(t, err) - - req.Header.Set("x-forwarded-host", "whoami.example.com") - req.Header.Set("x-forwarded-proto", "http") - req.Header.Set("x-forwarded-uri", "/") - - router.ServeHTTP(recorder, req) - assert.Equal(t, recorder.Code, http.StatusUnauthorized) - - // Test logged out user nginx (auth_request) - router, recorder = setupProxyController(t, nil) - - req, err = http.NewRequest("GET", "/api/auth/nginx", nil) - assert.NilError(t, err) - - req.Header.Set("x-original-url", "http://whoami.example.com/") - - router.ServeHTTP(recorder, req) - assert.Equal(t, recorder.Code, http.StatusUnauthorized) - - // Test logged out user envoy (ext_authz) - router, recorder = setupProxyController(t, nil) - - req, err = http.NewRequest("GET", "/api/auth/envoy?path=/", nil) - assert.NilError(t, err) - - req.Host = "whoami.example.com" - req.Header.Set("x-forwarded-proto", "http") - - router.ServeHTTP(recorder, req) - assert.Equal(t, recorder.Code, http.StatusUnauthorized) - - // Test logged in user traefik/caddy (forward_auth) - router, recorder = setupProxyController(t, []gin.HandlerFunc{ - func(c *gin.Context) { - c.Set("context", &loggedInCtx) - c.Next() - }, - }) - - req, err = http.NewRequest("GET", "/api/auth/traefik", nil) - assert.NilError(t, err) - - req.Header.Set("x-forwarded-host", "whoami.example.com") - req.Header.Set("x-forwarded-proto", "http") - req.Header.Set("x-forwarded-uri", "/") - - router.ServeHTTP(recorder, req) - assert.Equal(t, recorder.Code, http.StatusOK) - - // Test logged in user nginx (auth_request) - router, recorder = setupProxyController(t, []gin.HandlerFunc{ - func(c *gin.Context) { - c.Set("context", &loggedInCtx) - c.Next() - }, - }) - - req, err = http.NewRequest("GET", "/api/auth/nginx", nil) - assert.NilError(t, err) - - req.Header.Set("x-original-url", "http://whoami.example.com/") - - router.ServeHTTP(recorder, req) - assert.Equal(t, recorder.Code, http.StatusOK) - - // Test logged in user envoy (ext_authz) - router, recorder = setupProxyController(t, []gin.HandlerFunc{ - func(c *gin.Context) { - c.Set("context", &loggedInCtx) - c.Next() - }, - }) - - req, err = http.NewRequest("GET", "/api/auth/envoy?path=/", nil) - assert.NilError(t, err) - - req.Host = "whoami.example.com" - req.Header.Set("x-forwarded-proto", "http") - - router.ServeHTTP(recorder, req) - assert.Equal(t, recorder.Code, http.StatusOK) - - // Test ACL allow caddy/traefik (forward_auth) - router, recorder = setupProxyController(t, nil) - - req, err = http.NewRequest("GET", "/api/auth/traefik", nil) - assert.NilError(t, err) - - req.Header.Set("x-forwarded-host", "whoami.example.com") - req.Header.Set("x-forwarded-proto", "http") - req.Header.Set("x-forwarded-uri", "/allow") - - router.ServeHTTP(recorder, req) - assert.Equal(t, recorder.Code, http.StatusOK) - - // Test ACL allow nginx - router, recorder = setupProxyController(t, nil) - - req, err = http.NewRequest("GET", "/api/auth/nginx", nil) - assert.NilError(t, err) - - req.Header.Set("x-original-url", "http://whoami.example.com/allow") - - router.ServeHTTP(recorder, req) - assert.Equal(t, recorder.Code, http.StatusOK) - - // Test ACL allow envoy - router, recorder = setupProxyController(t, nil) - - req, err = http.NewRequest("GET", "/api/auth/envoy?path=/allow", nil) - assert.NilError(t, err) - - req.Host = "whoami.example.com" - req.Header.Set("x-forwarded-proto", "http") - - router.ServeHTTP(recorder, req) - assert.Equal(t, recorder.Code, http.StatusOK) - - // Test traefik/caddy (forward_auth) without required headers - router, recorder = setupProxyController(t, nil) - - req, err = http.NewRequest("GET", "/api/auth/traefik", nil) - assert.NilError(t, err) - - router.ServeHTTP(recorder, req) - assert.Equal(t, recorder.Code, http.StatusBadRequest) - - // Test nginx (forward_auth) without required headers - router, recorder = setupProxyController(t, nil) - - req, err = http.NewRequest("GET", "/api/auth/nginx", nil) - assert.NilError(t, err) - - router.ServeHTTP(recorder, req) - assert.Equal(t, recorder.Code, http.StatusBadRequest) - - // Test envoy (forward_auth) without required headers - router, recorder = setupProxyController(t, nil) - - req, err = http.NewRequest("GET", "/api/auth/envoy", nil) - assert.NilError(t, err) - - router.ServeHTTP(recorder, req) - assert.Equal(t, recorder.Code, http.StatusBadRequest) - - // Test nginx (auth_request) with forward_auth fallback with ACLs - router, recorder = setupProxyController(t, nil) - - req, err = http.NewRequest("GET", "/api/auth/nginx", nil) - assert.NilError(t, err) - - req.Header.Set("x-forwarded-host", "whoami.example.com") - req.Header.Set("x-forwarded-proto", "http") - req.Header.Set("x-forwarded-uri", "/allow") - - router.ServeHTTP(recorder, req) - assert.Equal(t, recorder.Code, http.StatusOK) - - // Test envoy (ext_authz) with forward_auth fallback with ACLs - router, recorder = setupProxyController(t, nil) - - req, err = http.NewRequest("GET", "/api/auth/envoy", nil) - assert.NilError(t, err) - - req.Header.Set("x-forwarded-host", "whoami.example.com") - req.Header.Set("x-forwarded-proto", "http") - req.Header.Set("x-forwarded-uri", "/allow") - - router.ServeHTTP(recorder, req) - assert.Equal(t, recorder.Code, http.StatusOK) - - // Test envoy (ext_authz) with empty path - router, recorder = setupProxyController(t, nil) - - req, err = http.NewRequest("GET", "/api/auth/envoy", nil) - assert.NilError(t, err) - - req.Host = "whoami.example.com" - req.Header.Set("x-forwarded-proto", "http") - - router.ServeHTTP(recorder, req) - assert.Equal(t, recorder.Code, http.StatusUnauthorized) - - // Ensure forward_auth fallback works with path (should ignore) - router, recorder = setupProxyController(t, nil) - - req, err = http.NewRequest("GET", "/api/auth/traefik?path=/allow", nil) - assert.NilError(t, err) - - req.Header.Set("x-forwarded-proto", "http") - req.Header.Set("x-forwarded-host", "whoami.example.com") - req.Header.Set("x-forwarded-uri", "/allow") - - router.ServeHTTP(recorder, req) - assert.Equal(t, recorder.Code, http.StatusOK) + acls := map[string]config.App{ + "app_path_allow": { + Config: config.AppConfig{ + Domain: "path-allow.example.com", + }, + Path: config.AppPath{ + Allow: "/allowed", + }, + }, + "app_user_allow": { + Config: config.AppConfig{ + Domain: "user-allow.example.com", + }, + Users: config.AppUsers{ + Allow: "testuser", + }, + }, + "ip_bypass": { + Config: config.AppConfig{ + Domain: "ip-bypass.example.com", + }, + IP: config.AppIP{ + Bypass: []string{"10.10.10.10"}, + }, + }, + } + + const browserUserAgent = ` + Mozilla/5.0 (Linux; Android 8.0.0; SM-G955U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Mobile Safari/537.36` + + simpleCtx := func(c *gin.Context) { + c.Set("context", &config.UserContext{ + Username: "testuser", + Name: "Testuser", + Email: "testuser@example.com", + IsLoggedIn: true, + Provider: "local", + }) + c.Next() + } + + simpleCtxTotp := func(c *gin.Context) { + c.Set("context", &config.UserContext{ + Username: "totpuser", + Name: "Totpuser", + Email: "totpuser@example.com", + IsLoggedIn: true, + Provider: "local", + TotpEnabled: true, + }) + c.Next() + } + + type testCase struct { + description string + middlewares []gin.HandlerFunc + run func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) + } + + tests := []testCase{ + { + description: "Default forward auth should be detected and used", + middlewares: []gin.HandlerFunc{}, + run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { + req := httptest.NewRequest("GET", "/api/auth/traefik", nil) + req.Header.Set("x-forwarded-host", "test.example.com") + req.Header.Set("x-forwarded-proto", "https") + req.Header.Set("x-forwarded-uri", "/") + req.Header.Set("user-agent", browserUserAgent) + router.ServeHTTP(recorder, req) + + assert.Equal(t, 307, recorder.Code) + location := recorder.Header().Get("Location") + assert.Contains(t, location, "https://tinyauth.example.com/login?redirect_uri=") + assert.Contains(t, location, "https%3A%2F%2Ftest.example.com%2F") + }, + }, + { + description: "Auth request (nginx) should be detected and used", + middlewares: []gin.HandlerFunc{}, + run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { + req := httptest.NewRequest("GET", "/api/auth/nginx", nil) + req.Header.Set("x-original-url", "https://test.example.com/") + router.ServeHTTP(recorder, req) + assert.Equal(t, 401, recorder.Code) + }, + }, + { + description: "Ext authz (envoy) should be detected and used", + middlewares: []gin.HandlerFunc{}, + run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { + req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/hello", nil) // test a different method for envoy + req.Host = "test.example.com" + req.Header.Set("x-forwarded-proto", "https") + router.ServeHTTP(recorder, req) + assert.Equal(t, 401, recorder.Code) + }, + }, + { + description: "Ensure forward auth fallback for nginx", + middlewares: []gin.HandlerFunc{}, + run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { + req := httptest.NewRequest("GET", "/api/auth/nginx", nil) + req.Header.Set("x-forwarded-host", "test.example.com") + req.Header.Set("x-forwarded-proto", "https") + req.Header.Set("x-forwarded-uri", "/") + router.ServeHTTP(recorder, req) + assert.Equal(t, 401, recorder.Code) + }, + }, + { + description: "Ensure forward auth fallback for envoy", + middlewares: []gin.HandlerFunc{}, + run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { + req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/hello", nil) + req.Header.Set("x-forwarded-host", "test.example.com") + req.Header.Set("x-forwarded-proto", "https") + req.Header.Set("x-forwarded-uri", "/hello") + router.ServeHTTP(recorder, req) + assert.Equal(t, 401, recorder.Code) + }, + }, + { + description: "Ensure normal authentication flow for forward auth", + middlewares: []gin.HandlerFunc{ + simpleCtx, + }, + run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { + req := httptest.NewRequest("GET", "/api/auth/traefik", nil) + req.Header.Set("x-forwarded-host", "test.example.com") + req.Header.Set("x-forwarded-proto", "https") + req.Header.Set("x-forwarded-uri", "/") + router.ServeHTTP(recorder, req) + + assert.Equal(t, 200, recorder.Code) + assert.Equal(t, "testuser", recorder.Header().Get("remote-user")) + assert.Equal(t, "Testuser", recorder.Header().Get("remote-name")) + assert.Equal(t, "testuser@example.com", recorder.Header().Get("remote-email")) + }, + }, + { + description: "Ensure normal authentication flow for nginx auth request", + middlewares: []gin.HandlerFunc{ + simpleCtx, + }, + run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { + req := httptest.NewRequest("GET", "/api/auth/nginx", nil) + req.Header.Set("x-original-url", "https://test.example.com/") + router.ServeHTTP(recorder, req) + + assert.Equal(t, 200, recorder.Code) + assert.Equal(t, "testuser", recorder.Header().Get("remote-user")) + assert.Equal(t, "Testuser", recorder.Header().Get("remote-name")) + assert.Equal(t, "testuser@example.com", recorder.Header().Get("remote-email")) + }, + }, + { + description: "Ensure normal authentication flow for envoy ext authz", + middlewares: []gin.HandlerFunc{ + simpleCtx, + }, + run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { + req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/hello", nil) + req.Host = "test.example.com" + req.Header.Set("x-forwarded-proto", "https") + router.ServeHTTP(recorder, req) + + assert.Equal(t, 200, recorder.Code) + assert.Equal(t, "testuser", recorder.Header().Get("remote-user")) + assert.Equal(t, "Testuser", recorder.Header().Get("remote-name")) + assert.Equal(t, "testuser@example.com", recorder.Header().Get("remote-email")) + }, + }, + { + description: "Ensure path allow ACL works on forward auth", + middlewares: []gin.HandlerFunc{}, + run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { + req := httptest.NewRequest("GET", "/api/auth/traefik", nil) + req.Header.Set("x-forwarded-host", "path-allow.example.com") + req.Header.Set("x-forwarded-proto", "https") + req.Header.Set("x-forwarded-uri", "/allowed") + router.ServeHTTP(recorder, req) + assert.Equal(t, 200, recorder.Code) + }, + }, + { + description: "Ensure path allow ACL works on nginx auth request", + middlewares: []gin.HandlerFunc{}, + run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { + req := httptest.NewRequest("GET", "/api/auth/nginx", nil) + req.Header.Set("x-original-url", "https://path-allow.example.com/allowed") + router.ServeHTTP(recorder, req) + assert.Equal(t, 200, recorder.Code) + }, + }, + { + description: "Ensure path allow ACL works on envoy ext authz", + middlewares: []gin.HandlerFunc{}, + run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { + req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/allowed", nil) + req.Host = "path-allow.example.com" + req.Header.Set("x-forwarded-proto", "https") + router.ServeHTTP(recorder, req) + assert.Equal(t, 200, recorder.Code) + }, + }, + { + description: "Ensure ip bypass ACL works on forward auth", + middlewares: []gin.HandlerFunc{}, + run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { + req := httptest.NewRequest("GET", "/api/auth/traefik", nil) + req.Header.Set("x-forwarded-host", "ip-bypass.example.com") + req.Header.Set("x-forwarded-proto", "https") + req.Header.Set("x-forwarded-uri", "/") + req.Header.Set("x-forwarded-for", "10.10.10.10") + router.ServeHTTP(recorder, req) + assert.Equal(t, 200, recorder.Code) + }, + }, + { + description: "Ensure ip bypass ACL works on nginx auth request", + middlewares: []gin.HandlerFunc{}, + run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { + req := httptest.NewRequest("GET", "/api/auth/nginx", nil) + req.Header.Set("x-original-url", "https://ip-bypass.example.com/") + req.Header.Set("x-forwarded-for", "10.10.10.10") + router.ServeHTTP(recorder, req) + assert.Equal(t, 200, recorder.Code) + }, + }, + { + description: "Ensure ip bypass ACL works on envoy ext authz", + middlewares: []gin.HandlerFunc{}, + run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { + req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/hello", nil) + req.Host = "ip-bypass.example.com" + req.Header.Set("x-forwarded-proto", "https") + req.Header.Set("x-forwarded-for", "10.10.10.10") + router.ServeHTTP(recorder, req) + assert.Equal(t, 200, recorder.Code) + }, + }, + { + description: "Ensure user allow ACL allows correct user (should allow testuer)", + middlewares: []gin.HandlerFunc{ + simpleCtx, + }, + run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { + req := httptest.NewRequest("GET", "/api/auth/traefik", nil) + req.Header.Set("x-forwarded-host", "user-allow.example.com") + req.Header.Set("x-forwarded-proto", "https") + req.Header.Set("x-forwarded-uri", "/") + router.ServeHTTP(recorder, req) + assert.Equal(t, 200, recorder.Code) + }, + }, + { + description: "Ensure user allow ACL blocks incorrect user (should block testuer)", + middlewares: []gin.HandlerFunc{ + simpleCtxTotp, + }, + run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { + req := httptest.NewRequest("GET", "/api/auth/traefik", nil) + req.Header.Set("x-forwarded-host", "user-allow.example.com") + req.Header.Set("x-forwarded-proto", "https") + req.Header.Set("x-forwarded-uri", "/") + router.ServeHTTP(recorder, req) + assert.Equal(t, 403, recorder.Code) + assert.Equal(t, "", recorder.Header().Get("remote-user")) + assert.Equal(t, "", recorder.Header().Get("remote-name")) + assert.Equal(t, "", recorder.Header().Get("remote-email")) + }, + }, + } + + tlog.NewSimpleLogger().Init() + + oauthBrokerCfgs := make(map[string]config.OAuthServiceConfig) + + app := bootstrap.NewBootstrapApp(config.Config{}) + + db, err := app.SetupDatabase("/tmp/tinyauth_test.db") + assert.NoError(t, err) + + queries := repository.New(db) + + docker := service.NewDockerService() + err = docker.Init() + assert.NoError(t, err) + + ldap := service.NewLdapService(service.LdapServiceConfig{}) + err = ldap.Init() + assert.NoError(t, err) + + broker := service.NewOAuthBrokerService(oauthBrokerCfgs) + err = broker.Init() + assert.NoError(t, err) + + authService := service.NewAuthService(authServiceCfg, docker, ldap, queries, broker) + err = authService.Init() + assert.NoError(t, err) + + aclsService := service.NewAccessControlsService(docker, acls) + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + router := gin.Default() + + for _, m := range test.middlewares { + router.Use(m) + } + + group := router.Group("/api") + gin.SetMode(gin.TestMode) + + recorder := httptest.NewRecorder() + + proxyController := controller.NewProxyController(controllerCfg, group, aclsService, authService) + proxyController.SetupRoutes() + + test.run(t, router, recorder) + }) + } + + err = db.Close() + assert.NoError(t, err) + + err = os.Remove("/tmp/tinyauth_test.db") + assert.NoError(t, err) }