From 78920cba642d1270dbc073bcba125b1803d16234 Mon Sep 17 00:00:00 2001 From: Stavros Date: Tue, 2 Sep 2025 18:19:18 +0300 Subject: [PATCH] feat: use decoded headers in proxy controller --- internal/config/config.go | 6 +- internal/controller/proxy_controller.go | 38 ++++++-- internal/utils/decoders/header_decoder.go | 8 +- .../utils/decoders/header_decoder_test.go | 86 ++++++++++--------- internal/utils/decoders/label_decoder.go | 8 +- internal/utils/decoders/label_decoder_test.go | 2 +- internal/utils/header_utils.go | 10 +++ 7 files changed, 96 insertions(+), 62 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 4a12116..fbe5aa6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -123,14 +123,12 @@ type RedirectQuery struct { RedirectURI string `url:"redirect_uri"` } -// Labels +// App config -type Labels struct { +type AppConfigs struct { Apps map[string]App } -// App config - type App struct { Config AppConfig Users AppUsers diff --git a/internal/controller/proxy_controller.go b/internal/controller/proxy_controller.go index 88eeb4d..dde799c 100644 --- a/internal/controller/proxy_controller.go +++ b/internal/controller/proxy_controller.go @@ -7,6 +7,7 @@ import ( "tinyauth/internal/config" "tinyauth/internal/service" "tinyauth/internal/utils" + "tinyauth/internal/utils/decoders" "github.com/gin-gonic/gin" "github.com/google/go-querystring/query" @@ -67,6 +68,16 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { proto := c.Request.Header.Get("X-Forwarded-Proto") host := c.Request.Header.Get("X-Forwarded-Host") + var app config.App + + headers, err := decoders.DecodeHeaders(utils.NormalizeHeaders(c.Request.Header)) + + if err != nil { + log.Error().Err(err).Msg("Failed to decode headers") + controller.handleError(c, req, isBrowser) + return + } + labels, err := controller.docker.GetLabels(host) if err != nil { @@ -75,10 +86,21 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { return } + if len(headers.Apps) > 0 { + for k, v := range headers.Apps { + log.Debug().Str("app", k).Msg("Using headers for app config instead of labels") + app = v + break + } + } else { + log.Debug().Msg("No app config found in headers, using labels") + app = labels + } + clientIP := c.ClientIP() - if controller.auth.IsBypassedIP(labels.IP, clientIP) { - controller.setHeaders(c, labels) + if controller.auth.IsBypassedIP(app.IP, clientIP) { + controller.setHeaders(c, app) c.JSON(200, gin.H{ "status": 200, "message": "Authenticated", @@ -86,7 +108,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { return } - authEnabled, err := controller.auth.IsAuthEnabled(uri, labels.Path) + authEnabled, err := controller.auth.IsAuthEnabled(uri, app.Path) if err != nil { log.Error().Err(err).Msg("Failed to check if auth is enabled for resource") @@ -96,7 +118,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { if !authEnabled { log.Debug().Msg("Authentication disabled for resource, allowing access") - controller.setHeaders(c, labels) + controller.setHeaders(c, app) c.JSON(200, gin.H{ "status": 200, "message": "Authenticated", @@ -104,7 +126,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { return } - if !controller.auth.CheckIP(labels.IP, clientIP) { + if !controller.auth.CheckIP(app.IP, clientIP) { if req.Proxy == "nginx" || !isBrowser { c.JSON(401, gin.H{ "status": 401, @@ -147,7 +169,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { } if userContext.IsLoggedIn { - appAllowed := controller.auth.IsResourceAllowed(c, userContext, labels) + appAllowed := controller.auth.IsResourceAllowed(c, userContext, app) if !appAllowed { log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User not allowed to access resource") @@ -181,7 +203,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { } if userContext.OAuth { - groupOK := controller.auth.IsInOAuthGroup(c, userContext, labels.OAuth.Groups) + groupOK := controller.auth.IsInOAuthGroup(c, userContext, app.OAuth.Groups) if !groupOK { log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User OAuth groups do not match resource requirements") @@ -221,7 +243,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { c.Header("Remote-Email", utils.SanitizeHeader(userContext.Email)) c.Header("Remote-Groups", utils.SanitizeHeader(userContext.OAuthGroups)) - controller.setHeaders(c, labels) + controller.setHeaders(c, app) c.JSON(200, gin.H{ "status": 200, diff --git a/internal/utils/decoders/header_decoder.go b/internal/utils/decoders/header_decoder.go index 46ab045..dd0bf41 100644 --- a/internal/utils/decoders/header_decoder.go +++ b/internal/utils/decoders/header_decoder.go @@ -11,13 +11,13 @@ import ( // Based on: https://github.com/traefik/paerser/blob/master/parser/labels_decode.go (Apache 2.0 License) -func DecodeHeaders(headers map[string]string) (config.App, error) { - var app config.App +func DecodeHeaders(headers map[string]string) (config.AppConfigs, error) { + var app config.AppConfigs - err := decodeHeadersHelper(headers, &app, "tinyauth") + err := decodeHeadersHelper(headers, &app, "tinyauth", "tinyauth-apps") if err != nil { - return config.App{}, err + return config.AppConfigs{}, err } return app, nil diff --git a/internal/utils/decoders/header_decoder_test.go b/internal/utils/decoders/header_decoder_test.go index 1467a78..02d0990 100644 --- a/internal/utils/decoders/header_decoder_test.go +++ b/internal/utils/decoders/header_decoder_test.go @@ -9,51 +9,55 @@ import ( func TestDecodeHeaders(t *testing.T) { // Variables - expected := config.App{ - Config: config.AppConfig{ - Domain: "example.com", - }, - Users: config.AppUsers{ - Allow: "user1,user2", - Block: "user3", - }, - OAuth: config.AppOAuth{ - Whitelist: "somebody@example.com", - Groups: "group3", - }, - IP: config.AppIP{ - Allow: []string{"10.71.0.1/24", "10.71.0.2"}, - Block: []string{"10.10.10.10", "10.0.0.0/24"}, - Bypass: []string{"192.168.1.1"}, - }, - Response: config.AppResponse{ - Headers: []string{"X-Foo=Bar", "X-Baz=Qux"}, - BasicAuth: config.AppBasicAuth{ - Username: "admin", - Password: "password", - PasswordFile: "/path/to/passwordfile", + expected := config.AppConfigs{ + Apps: map[string]config.App{ + "foo": { + Config: config.AppConfig{ + Domain: "example.com", + }, + Users: config.AppUsers{ + Allow: "user1,user2", + Block: "user3", + }, + OAuth: config.AppOAuth{ + Whitelist: "somebody@example.com", + Groups: "group3", + }, + IP: config.AppIP{ + Allow: []string{"10.71.0.1/24", "10.71.0.2"}, + Block: []string{"10.10.10.10", "10.0.0.0/24"}, + Bypass: []string{"192.168.1.1"}, + }, + Response: config.AppResponse{ + Headers: []string{"X-Foo=Bar", "X-Baz=Qux"}, + BasicAuth: config.AppBasicAuth{ + Username: "admin", + Password: "password", + PasswordFile: "/path/to/passwordfile", + }, + }, + Path: config.AppPath{ + Allow: "/public", + Block: "/private", + }, }, }, - Path: config.AppPath{ - Allow: "/public", - Block: "/private", - }, } test := map[string]string{ - "Tinyauth-Config-Domain": "example.com", - "Tinyauth-Users-Allow": "user1,user2", - "Tinyauth-Users-Block": "user3", - "Tinyauth-OAuth-Whitelist": "somebody@example.com", - "Tinyauth-OAuth-Groups": "group3", - "Tinyauth-IP-Allow": "10.71.0.1/24,10.71.0.2", - "Tinyauth-IP-Block": "10.10.10.10,10.0.0.0/24", - "Tinyauth-IP-Bypass": "192.168.1.1", - "Tinyauth-Response-Headers": "X-Foo=Bar,X-Baz=Qux", - "Tinyauth-Response-BasicAuth-Username": "admin", - "Tinyauth-Response-BasicAuth-Password": "password", - "Tinyauth-Response-BasicAuth-PasswordFile": "/path/to/passwordfile", - "Tinyauth-Path-Allow": "/public", - "Tinyauth-Path-Block": "/private", + "Tinyauth-Apps-Foo-Config-Domain": "example.com", + "Tinyauth-Apps-Foo-Users-Allow": "user1,user2", + "Tinyauth-Apps-Foo-Users-Block": "user3", + "Tinyauth-Apps-Foo-OAuth-Whitelist": "somebody@example.com", + "Tinyauth-Apps-Foo-OAuth-Groups": "group3", + "Tinyauth-Apps-Foo-IP-Allow": "10.71.0.1/24,10.71.0.2", + "Tinyauth-Apps-Foo-IP-Block": "10.10.10.10,10.0.0.0/24", + "Tinyauth-Apps-Foo-IP-Bypass": "192.168.1.1", + "Tinyauth-Apps-Foo-Response-Headers": "X-Foo=Bar,X-Baz=Qux", + "Tinyauth-Apps-Foo-Response-BasicAuth-Username": "admin", + "Tinyauth-Apps-Foo-Response-BasicAuth-Password": "password", + "Tinyauth-Apps-Foo-Response-BasicAuth-PasswordFile": "/path/to/passwordfile", + "Tinyauth-Apps-Foo-Path-Allow": "/public", + "Tinyauth-Apps-Foo-Path-Block": "/private", } // Test diff --git a/internal/utils/decoders/label_decoder.go b/internal/utils/decoders/label_decoder.go index 21e6bbf..fbb7ecb 100644 --- a/internal/utils/decoders/label_decoder.go +++ b/internal/utils/decoders/label_decoder.go @@ -6,13 +6,13 @@ import ( "github.com/traefik/paerser/parser" ) -func DecodeLabels(labels map[string]string) (config.Labels, error) { - var appLabels config.Labels +func DecodeLabels(labels map[string]string) (config.AppConfigs, error) { + var appLabels config.AppConfigs - err := parser.Decode(labels, &appLabels, "tinyauth") + err := parser.Decode(labels, &appLabels, "tinyauth", "tinyauth.apps") if err != nil { - return config.Labels{}, err + return config.AppConfigs{}, err } return appLabels, nil diff --git a/internal/utils/decoders/label_decoder_test.go b/internal/utils/decoders/label_decoder_test.go index 73e6eec..558860a 100644 --- a/internal/utils/decoders/label_decoder_test.go +++ b/internal/utils/decoders/label_decoder_test.go @@ -9,7 +9,7 @@ import ( func TestDecodeLabels(t *testing.T) { // Variables - expected := config.Labels{ + expected := config.AppConfigs{ Apps: map[string]config.App{ "foo": { Config: config.AppConfig{ diff --git a/internal/utils/header_utils.go b/internal/utils/header_utils.go index 2ef9a70..79aeb33 100644 --- a/internal/utils/header_utils.go +++ b/internal/utils/header_utils.go @@ -32,3 +32,13 @@ func SanitizeHeader(header string) string { return -1 }, header) } + +func NormalizeHeaders(headers http.Header) map[string]string { + var result = make(map[string]string) + + for key, values := range headers { + result[key] = strings.Join(values, ",") + } + + return result +}