diff --git a/internal/controller/proxy_controller.go b/internal/controller/proxy_controller.go index b7958080..4be21258 100644 --- a/internal/controller/proxy_controller.go +++ b/internal/controller/proxy_controller.go @@ -134,16 +134,6 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { return } - if controller.policyEngine.Evaluate(service.RuleAuthEnabled, aclsCtx) { - controller.log.App.Debug().Msg("Authentication is disabled for this resource, allowing access without authentication") - controller.setHeaders(c, acls) - c.JSON(200, gin.H{ - "status": 200, - "message": "Authenticated", - }) - return - } - if !controller.policyEngine.Evaluate(service.RuleIPAllowed, aclsCtx) { queries, err := query.Values(UnauthorizedQuery{ Resource: strings.Split(proxyCtx.Host, ".")[0], @@ -171,6 +161,16 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { return } + if controller.policyEngine.Evaluate(service.RuleAuthEnabled, aclsCtx) { + controller.log.App.Debug().Msg("Authentication is disabled for this resource, allowing access without authentication") + controller.setHeaders(c, acls) + c.JSON(200, gin.H{ + "status": 200, + "message": "Authenticated", + }) + return + } + userContext, err := new(model.UserContext).NewFromGin(c) if err != nil { diff --git a/internal/controller/proxy_controller_test.go b/internal/controller/proxy_controller_test.go index f45431a0..df606e2d 100644 --- a/internal/controller/proxy_controller_test.go +++ b/internal/controller/proxy_controller_test.go @@ -25,6 +25,7 @@ func TestProxyController(t *testing.T) { log.Init() cfg, runtime := test.CreateTestConfigs(t) + cfg.Auth.IP.Block = []string{"198.51.100.9"} 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` @@ -510,6 +511,94 @@ func TestProxyController(t *testing.T) { assert.Equal(t, http.StatusUnauthorized, recorder.Code) }, }, + { + description: "App IP block takes precedence over path rules", + 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", "acl-precedence-block.example.com") + req.Header.Set("x-forwarded-proto", "https") + req.Header.Set("x-forwarded-uri", "/allowed") + req.Header.Set("x-forwarded-for", "10.10.10.10") + router.ServeHTTP(recorder, req) + + assert.Equal(t, http.StatusForbidden, recorder.Code) + assert.Contains(t, recorder.Header().Get("x-tinyauth-location"), "ip=10.10.10.10") + }, + }, + { + description: "App IP allow list takes precedence over path rules", + 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", "acl-precedence-allow.example.com") + req.Header.Set("x-forwarded-proto", "https") + req.Header.Set("x-forwarded-uri", "/allowed") + req.Header.Set("x-forwarded-for", "10.10.10.11") + router.ServeHTTP(recorder, req) + + assert.Equal(t, http.StatusForbidden, recorder.Code) + assert.Contains(t, recorder.Header().Get("x-tinyauth-location"), "ip=10.10.10.11") + }, + }, + { + description: "Global IP block takes precedence over path rules", + 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", "acl-precedence-global.example.com") + req.Header.Set("x-forwarded-proto", "https") + req.Header.Set("x-forwarded-uri", "/allowed") + req.Header.Set("x-forwarded-for", "198.51.100.9") + router.ServeHTTP(recorder, req) + + assert.Equal(t, http.StatusForbidden, recorder.Code) + assert.Contains(t, recorder.Header().Get("x-tinyauth-location"), "ip=198.51.100.9") + }, + }, + { + description: "App IP block takes precedence over path block rules", + 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", "acl-precedence-path-block.example.com") + req.Header.Set("x-forwarded-proto", "https") + req.Header.Set("x-forwarded-uri", "/public") + req.Header.Set("x-forwarded-for", "10.10.10.10") + router.ServeHTTP(recorder, req) + + assert.Equal(t, http.StatusForbidden, recorder.Code) + assert.Contains(t, recorder.Header().Get("x-tinyauth-location"), "ip=10.10.10.10") + }, + }, + { + description: "Path rules allow unrestricted IPs", + 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", "acl-precedence-block.example.com") + req.Header.Set("x-forwarded-proto", "https") + req.Header.Set("x-forwarded-uri", "/allowed") + req.Header.Set("x-forwarded-for", "10.10.10.11") + router.ServeHTTP(recorder, req) + + assert.Equal(t, http.StatusOK, recorder.Code) + }, + }, + { + description: "IP bypass retains highest precedence", + 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", "acl-precedence-bypass.example.com") + req.Header.Set("x-forwarded-proto", "https") + req.Header.Set("x-forwarded-uri", "/allowed") + req.Header.Set("x-forwarded-for", "10.10.10.10") + router.ServeHTTP(recorder, req) + + assert.Equal(t, http.StatusOK, recorder.Code) + }, + }, { description: "Ensure ip bypass ACL works on forward auth", middlewares: []gin.HandlerFunc{}, diff --git a/internal/test/test.go b/internal/test/test.go index 6ff41a88..e239eb78 100644 --- a/internal/test/test.go +++ b/internal/test/test.go @@ -76,6 +76,59 @@ func CreateTestConfigs(t *testing.T) (model.Config, model.RuntimeConfig) { Block: "^/admin$", }, }, + "app_acl_precedence_block": { + Config: model.AppConfig{ + Domain: "acl-precedence-block.example.com", + }, + Path: model.AppPath{ + Allow: "^/allowed$", + }, + IP: model.AppIP{ + Block: []string{"10.10.10.10"}, + }, + }, + "app_acl_precedence_allow": { + Config: model.AppConfig{ + Domain: "acl-precedence-allow.example.com", + }, + Path: model.AppPath{ + Allow: "^/allowed$", + }, + IP: model.AppIP{ + Allow: []string{"10.10.10.10"}, + }, + }, + "app_acl_precedence_global": { + Config: model.AppConfig{ + Domain: "acl-precedence-global.example.com", + }, + Path: model.AppPath{ + Allow: "^/allowed$", + }, + }, + "app_acl_precedence_path_block": { + Config: model.AppConfig{ + Domain: "acl-precedence-path-block.example.com", + }, + Path: model.AppPath{ + Block: "^/private$", + }, + IP: model.AppIP{ + Block: []string{"10.10.10.10"}, + }, + }, + "app_acl_precedence_bypass": { + Config: model.AppConfig{ + Domain: "acl-precedence-bypass.example.com", + }, + Path: model.AppPath{ + Allow: "^/allowed$", + }, + IP: model.AppIP{ + Block: []string{"10.10.10.10"}, + Bypass: []string{"10.10.10.10"}, + }, + }, "app_user_allow": { Config: model.AppConfig{ Domain: "user-allow.example.com",