From f3eb7f69b464db3a4b77fbb872626fc9a6bf9040 Mon Sep 17 00:00:00 2001 From: Stavros Date: Wed, 3 Sep 2025 12:12:18 +0300 Subject: [PATCH] Revert "feat: header based acls (#337)" (#340) This reverts commit f0d2da281aee1ea2b2e7baafd28521e2368fc3cf. --- internal/config/config.go | 36 +++--- internal/controller/proxy_controller.go | 40 ++---- internal/service/auth_service.go | 8 +- internal/service/docker_service.go | 12 +- internal/utils/decoders/header_decoder.go | 119 ------------------ .../utils/decoders/header_decoder_test.go | 73 ----------- internal/utils/decoders/label_decoder.go | 19 --- internal/utils/decoders/label_decoder_test.go | 73 ----------- .../utils/{header_utils.go => label_utils.go} | 24 ++-- internal/utils/security_utils.go | 2 - 10 files changed, 51 insertions(+), 355 deletions(-) delete mode 100644 internal/utils/decoders/header_decoder.go delete mode 100644 internal/utils/decoders/header_decoder_test.go delete mode 100644 internal/utils/decoders/label_decoder.go delete mode 100644 internal/utils/decoders/label_decoder_test.go rename internal/utils/{header_utils.go => label_utils.go} (73%) diff --git a/internal/config/config.go b/internal/config/config.go index fbe5aa6..82050de 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -123,53 +123,53 @@ type RedirectQuery struct { RedirectURI string `url:"redirect_uri"` } -// App config +// Labels -type AppConfigs struct { - Apps map[string]App +type Labels struct { + Apps map[string]AppLabels } -type App struct { - Config AppConfig - Users AppUsers - OAuth AppOAuth - IP AppIP - Response AppResponse - Path AppPath +type AppLabels struct { + Config ConfigLabels + Users UsersLabels + OAuth OAuthLabels + IP IPLabels + Response ResponseLabels + Path PathLabels } -type AppConfig struct { +type ConfigLabels struct { Domain string } -type AppUsers struct { +type UsersLabels struct { Allow string Block string } -type AppOAuth struct { +type OAuthLabels struct { Whitelist string Groups string } -type AppIP struct { +type IPLabels struct { Allow []string Block []string Bypass []string } -type AppResponse struct { +type ResponseLabels struct { Headers []string - BasicAuth AppBasicAuth + BasicAuth BasicAuthLabels } -type AppBasicAuth struct { +type BasicAuthLabels struct { Username string Password string PasswordFile string } -type AppPath struct { +type PathLabels struct { Allow string Block string } diff --git a/internal/controller/proxy_controller.go b/internal/controller/proxy_controller.go index dde799c..fd25076 100644 --- a/internal/controller/proxy_controller.go +++ b/internal/controller/proxy_controller.go @@ -7,7 +7,6 @@ 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" @@ -68,16 +67,6 @@ 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 { @@ -86,21 +75,10 @@ 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(app.IP, clientIP) { - controller.setHeaders(c, app) + if controller.auth.IsBypassedIP(labels.IP, clientIP) { + controller.setHeaders(c, labels) c.JSON(200, gin.H{ "status": 200, "message": "Authenticated", @@ -108,7 +86,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { return } - authEnabled, err := controller.auth.IsAuthEnabled(uri, app.Path) + authEnabled, err := controller.auth.IsAuthEnabled(uri, labels.Path) if err != nil { log.Error().Err(err).Msg("Failed to check if auth is enabled for resource") @@ -118,7 +96,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { if !authEnabled { log.Debug().Msg("Authentication disabled for resource, allowing access") - controller.setHeaders(c, app) + controller.setHeaders(c, labels) c.JSON(200, gin.H{ "status": 200, "message": "Authenticated", @@ -126,7 +104,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { return } - if !controller.auth.CheckIP(app.IP, clientIP) { + if !controller.auth.CheckIP(labels.IP, clientIP) { if req.Proxy == "nginx" || !isBrowser { c.JSON(401, gin.H{ "status": 401, @@ -169,7 +147,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { } if userContext.IsLoggedIn { - appAllowed := controller.auth.IsResourceAllowed(c, userContext, app) + appAllowed := controller.auth.IsResourceAllowed(c, userContext, labels) if !appAllowed { log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User not allowed to access resource") @@ -203,7 +181,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { } if userContext.OAuth { - groupOK := controller.auth.IsInOAuthGroup(c, userContext, app.OAuth.Groups) + groupOK := controller.auth.IsInOAuthGroup(c, userContext, labels.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") @@ -243,7 +221,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, app) + controller.setHeaders(c, labels) c.JSON(200, gin.H{ "status": 200, @@ -273,7 +251,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/login?%s", controller.config.AppURL, queries.Encode())) } -func (controller *ProxyController) setHeaders(c *gin.Context, labels config.App) { +func (controller *ProxyController) setHeaders(c *gin.Context, labels config.AppLabels) { c.Header("Authorization", c.Request.Header.Get("Authorization")) headers := utils.ParseHeaders(labels.Response.Headers) diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index 9739cb9..cb14a7e 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -285,7 +285,7 @@ func (auth *AuthService) UserAuthConfigured() bool { return len(auth.config.Users) > 0 || auth.ldap != nil } -func (auth *AuthService) IsResourceAllowed(c *gin.Context, context config.UserContext, labels config.App) bool { +func (auth *AuthService) IsResourceAllowed(c *gin.Context, context config.UserContext, labels config.AppLabels) bool { if context.OAuth { log.Debug().Msg("Checking OAuth whitelist") return utils.CheckFilter(labels.OAuth.Whitelist, context.Email) @@ -322,7 +322,7 @@ func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context config.UserConte return false } -func (auth *AuthService) IsAuthEnabled(uri string, path config.AppPath) (bool, error) { +func (auth *AuthService) IsAuthEnabled(uri string, path config.PathLabels) (bool, error) { // Check for block list if path.Block != "" { regex, err := regexp.Compile(path.Block) @@ -364,7 +364,7 @@ func (auth *AuthService) GetBasicAuth(c *gin.Context) *config.User { } } -func (auth *AuthService) CheckIP(labels config.AppIP, ip string) bool { +func (auth *AuthService) CheckIP(labels config.IPLabels, ip string) bool { for _, blocked := range labels.Block { res, err := utils.FilterIP(blocked, ip) if err != nil { @@ -398,7 +398,7 @@ func (auth *AuthService) CheckIP(labels config.AppIP, ip string) bool { return true } -func (auth *AuthService) IsBypassedIP(labels config.AppIP, ip string) bool { +func (auth *AuthService) IsBypassedIP(labels config.IPLabels, ip string) bool { for _, bypassed := range labels.Bypass { res, err := utils.FilterIP(bypassed, ip) if err != nil { diff --git a/internal/service/docker_service.go b/internal/service/docker_service.go index d2a4cfc..f4ce236 100644 --- a/internal/service/docker_service.go +++ b/internal/service/docker_service.go @@ -4,7 +4,7 @@ import ( "context" "strings" "tinyauth/internal/config" - "tinyauth/internal/utils/decoders" + "tinyauth/internal/utils" container "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" @@ -55,17 +55,17 @@ func (docker *DockerService) DockerConnected() bool { return err == nil } -func (docker *DockerService) GetLabels(appDomain string) (config.App, error) { +func (docker *DockerService) GetLabels(appDomain string) (config.AppLabels, error) { isConnected := docker.DockerConnected() if !isConnected { log.Debug().Msg("Docker not connected, returning empty labels") - return config.App{}, nil + return config.AppLabels{}, nil } containers, err := docker.GetContainers() if err != nil { - return config.App{}, err + return config.AppLabels{}, err } for _, ctr := range containers { @@ -75,7 +75,7 @@ func (docker *DockerService) GetLabels(appDomain string) (config.App, error) { continue } - labels, err := decoders.DecodeLabels(inspect.Config.Labels) + labels, err := utils.GetLabels(inspect.Config.Labels) if err != nil { log.Warn().Str("id", ctr.ID).Err(err).Msg("Error getting container labels, skipping") continue @@ -95,5 +95,5 @@ func (docker *DockerService) GetLabels(appDomain string) (config.App, error) { } log.Debug().Msg("No matching container found, returning empty labels") - return config.App{}, nil + return config.AppLabels{}, nil } diff --git a/internal/utils/decoders/header_decoder.go b/internal/utils/decoders/header_decoder.go deleted file mode 100644 index 834b845..0000000 --- a/internal/utils/decoders/header_decoder.go +++ /dev/null @@ -1,119 +0,0 @@ -package decoders - -import ( - "fmt" - "sort" - "strings" - "tinyauth/internal/config" - - "github.com/traefik/paerser/parser" -) - -// Based on: https://github.com/traefik/paerser/blob/master/parser/labels_decode.go (Apache 2.0 License) - -func DecodeHeaders(headers map[string]string) (config.AppConfigs, error) { - var app config.AppConfigs - - err := decodeHeadersHelper(headers, &app, "tinyauth", "tinyauth-apps") - - if err != nil { - return config.AppConfigs{}, err - } - - return app, nil -} - -func decodeHeadersHelper(headers map[string]string, element any, rootName string, filters ...string) error { - node, err := decodeHeadersToNode(headers, rootName, filters...) - - if err != nil { - return err - } - - opts := parser.MetadataOpts{TagName: "header", AllowSliceAsStruct: true} - err = parser.AddMetadata(element, node, opts) - - if err != nil { - return err - } - - return parser.Fill(element, node, parser.FillerOpts{AllowSliceAsStruct: true}) -} - -func decodeHeadersToNode(headers map[string]string, rootName string, filters ...string) (*parser.Node, error) { - sortedKeys := sortKeys(headers, filters) - - var node *parser.Node - - for i, key := range sortedKeys { - split := strings.Split(strings.ToLower(key), "-") - - if split[0] != rootName { - return nil, fmt.Errorf("invalid header root %s", split[0]) - } - - for _, v := range split { - if v == "" { - return nil, fmt.Errorf("invalid element: %s", key) - } - } - - if i == 0 { - node = &parser.Node{} - } - - decodeHeaderToNode(node, split, headers[key]) - } - - return node, nil -} - -func decodeHeaderToNode(root *parser.Node, path []string, value string) { - if len(root.Name) == 0 { - root.Name = path[0] - } - - if len(path) > 1 { - node := containsNode(root.Children, path[1]) - - if node != nil { - decodeHeaderToNode(node, path[1:], value) - } else { - child := &parser.Node{Name: path[1]} - decodeHeaderToNode(child, path[1:], value) - root.Children = append(root.Children, child) - } - } else { - root.Value = value - } -} - -func containsNode(nodes []*parser.Node, name string) *parser.Node { - for _, node := range nodes { - if strings.EqualFold(node.Name, name) { - return node - } - } - return nil -} - -func sortKeys(headers map[string]string, filters []string) []string { - var sortedKeys []string - - for key := range headers { - if len(filters) == 0 { - sortedKeys = append(sortedKeys, key) - continue - } - - for _, filter := range filters { - if strings.HasPrefix(strings.ToLower(key), strings.ToLower(filter)) { - sortedKeys = append(sortedKeys, key) - continue - } - } - } - - sort.Strings(sortedKeys) - return sortedKeys -} diff --git a/internal/utils/decoders/header_decoder_test.go b/internal/utils/decoders/header_decoder_test.go deleted file mode 100644 index 02d0990..0000000 --- a/internal/utils/decoders/header_decoder_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package decoders_test - -import ( - "reflect" - "testing" - "tinyauth/internal/config" - "tinyauth/internal/utils/decoders" -) - -func TestDecodeHeaders(t *testing.T) { - // Variables - 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", - }, - }, - }, - } - test := map[string]string{ - "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 - result, err := decoders.DecodeHeaders(test) - - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - - if !reflect.DeepEqual(expected, result) { - t.Fatalf("Expected %v but got %v", expected, result) - } -} diff --git a/internal/utils/decoders/label_decoder.go b/internal/utils/decoders/label_decoder.go deleted file mode 100644 index fbb7ecb..0000000 --- a/internal/utils/decoders/label_decoder.go +++ /dev/null @@ -1,19 +0,0 @@ -package decoders - -import ( - "tinyauth/internal/config" - - "github.com/traefik/paerser/parser" -) - -func DecodeLabels(labels map[string]string) (config.AppConfigs, error) { - var appLabels config.AppConfigs - - err := parser.Decode(labels, &appLabels, "tinyauth", "tinyauth.apps") - - if err != nil { - 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 deleted file mode 100644 index 558860a..0000000 --- a/internal/utils/decoders/label_decoder_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package decoders_test - -import ( - "reflect" - "testing" - "tinyauth/internal/config" - "tinyauth/internal/utils/decoders" -) - -func TestDecodeLabels(t *testing.T) { - // Variables - 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", - }, - }, - }, - } - test := map[string]string{ - "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 - result, err := decoders.DecodeLabels(test) - - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - - if reflect.DeepEqual(expected, result) == false { - t.Fatalf("Expected %v but got %v", expected, result) - } -} diff --git a/internal/utils/header_utils.go b/internal/utils/label_utils.go similarity index 73% rename from internal/utils/header_utils.go rename to internal/utils/label_utils.go index 79aeb33..5e423f7 100644 --- a/internal/utils/header_utils.go +++ b/internal/utils/label_utils.go @@ -3,8 +3,22 @@ package utils import ( "net/http" "strings" + "tinyauth/internal/config" + + "github.com/traefik/paerser/parser" ) +func GetLabels(labels map[string]string) (config.Labels, error) { + var labelsParsed config.Labels + + err := parser.Decode(labels, &labelsParsed, "tinyauth", "tinyauth.apps") + if err != nil { + return config.Labels{}, err + } + + return labelsParsed, nil +} + func ParseHeaders(headers []string) map[string]string { headerMap := make(map[string]string) for _, header := range headers { @@ -32,13 +46,3 @@ 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 -} diff --git a/internal/utils/security_utils.go b/internal/utils/security_utils.go index e1f1b2e..b40c56c 100644 --- a/internal/utils/security_utils.go +++ b/internal/utils/security_utils.go @@ -46,8 +46,6 @@ func GetBasicAuth(username string, password string) string { } func FilterIP(filter string, ip string) (bool, error) { - filter = strings.Replace(filter, "-", "/", -1) - ipAddr := net.ParseIP(ip) if strings.Contains(filter, "/") {