diff --git a/Dockerfile b/Dockerfile index bef09ed..89d10e8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Site builder -FROM oven/bun:1.2.15-alpine AS frontend-builder +FROM oven/bun:1.2.16-alpine AS frontend-builder WORKDIR /frontend diff --git a/frontend/Dockerfile.dev b/frontend/Dockerfile.dev index c8210ba..0584f8c 100644 --- a/frontend/Dockerfile.dev +++ b/frontend/Dockerfile.dev @@ -1,4 +1,4 @@ -FROM oven/bun:1.1.45-alpine +FROM oven/bun:1.2.16-alpine WORKDIR /frontend diff --git a/go.mod b/go.mod index 8bd3754..ce4a327 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( github.com/moby/sys/atomicwriter v0.1.0 // indirect github.com/moby/term v0.5.2 // indirect github.com/morikuni/aec v1.0.0 // indirect + github.com/traefik/paerser v0.2.2 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect diff --git a/go.sum b/go.sum index 6000b80..d95b178 100644 --- a/go.sum +++ b/go.sum @@ -238,6 +238,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/traefik/paerser v0.2.2 h1:cpzW/ZrQrBh3mdwD/jnp6aXASiUFKOVr6ldP+keJTcQ= +github.com/traefik/paerser v0.2.2/go.mod h1:7BBDd4FANoVgaTZG+yh26jI6CA2nds7D/4VTEdIsh24= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= diff --git a/internal/auth/auth.go b/internal/auth/auth.go index b416a88..9f09d24 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -264,11 +264,11 @@ func (auth *Auth) UserAuthConfigured() bool { return len(auth.Config.Users) > 0 } -func (auth *Auth) ResourceAllowed(c *gin.Context, context types.UserContext, labels types.TinyauthLabels) bool { +func (auth *Auth) ResourceAllowed(c *gin.Context, context types.UserContext, labels types.Labels) bool { // Check if oauth is allowed if context.OAuth { log.Debug().Msg("Checking OAuth whitelist") - return utils.CheckWhitelist(labels.OAuthWhitelist, context.Email) + return utils.CheckWhitelist(labels.OAuth.Whitelist, context.Email) } // Check users @@ -277,9 +277,9 @@ func (auth *Auth) ResourceAllowed(c *gin.Context, context types.UserContext, lab return utils.CheckWhitelist(labels.Users, context.Username) } -func (auth *Auth) OAuthGroup(c *gin.Context, context types.UserContext, labels types.TinyauthLabels) bool { +func (auth *Auth) OAuthGroup(c *gin.Context, context types.UserContext, labels types.Labels) bool { // Check if groups are required - if labels.OAuthGroups == "" { + if labels.OAuth.Groups == "" { return true } @@ -294,7 +294,7 @@ func (auth *Auth) OAuthGroup(c *gin.Context, context types.UserContext, labels t // For every group check if it is in the required groups for _, group := range oauthGroups { - if utils.CheckWhitelist(labels.OAuthGroups, group) { + if utils.CheckWhitelist(labels.OAuth.Groups, group) { log.Debug().Str("group", group).Msg("Group is in required groups") return true } @@ -307,7 +307,7 @@ func (auth *Auth) OAuthGroup(c *gin.Context, context types.UserContext, labels t return false } -func (auth *Auth) AuthEnabled(c *gin.Context, labels types.TinyauthLabels) (bool, error) { +func (auth *Auth) AuthEnabled(c *gin.Context, labels types.Labels) (bool, error) { // Get headers uri := c.Request.Header.Get("X-Forwarded-Uri") diff --git a/internal/constants/constants.go b/internal/constants/constants.go index b01001c..0d75366 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -1,14 +1,5 @@ package constants -// TinyauthLabels is a list of labels that can be used in a tinyauth protected container -var TinyauthLabels = []string{ - "tinyauth.oauth.whitelist", - "tinyauth.users", - "tinyauth.allowed", - "tinyauth.headers", - "tinyauth.oauth.groups", -} - // Claims are the OIDC supported claims (including preferd username for some reason) type Claims struct { Name string `json:"name"` diff --git a/internal/docker/docker.go b/internal/docker/docker.go index 170de23..684ca0a 100644 --- a/internal/docker/docker.go +++ b/internal/docker/docker.go @@ -74,14 +74,14 @@ func (docker *Docker) DockerConnected() bool { return err == nil } -func (docker *Docker) GetLabels(appId string) (types.TinyauthLabels, error) { +func (docker *Docker) GetLabels(appId string) (types.Labels, error) { // Check if we have access to the Docker API isConnected := docker.DockerConnected() // If we don't have access, return an empty struct if !isConnected { log.Debug().Msg("Docker not connected, returning empty labels") - return types.TinyauthLabels{}, nil + return types.Labels{}, nil } // Get the containers @@ -89,7 +89,7 @@ func (docker *Docker) GetLabels(appId string) (types.TinyauthLabels, error) { // If there is an error, return false if err != nil { - return types.TinyauthLabels{}, err + return types.Labels{}, err } log.Debug().Msg("Got containers") @@ -99,9 +99,10 @@ func (docker *Docker) GetLabels(appId string) (types.TinyauthLabels, error) { // Inspect the container inspect, err := docker.InspectContainer(container.ID) - // If there is an error, return false + // Check if there was an error if err != nil { - return types.TinyauthLabels{}, err + log.Warn().Str("id", container.ID).Err(err).Msg("Error inspecting container, skipping") + continue } // Get the container name (for some reason it is /name) @@ -112,7 +113,13 @@ func (docker *Docker) GetLabels(appId string) (types.TinyauthLabels, error) { log.Debug().Str("container", containerName).Msg("Found container") // Get only the tinyauth labels in a struct - labels := utils.GetTinyauthLabels(inspect.Config.Labels) + labels, err := utils.GetLabels(inspect.Config.Labels) + + // Check if there was an error + if err != nil { + log.Error().Err(err).Msg("Error parsing labels") + return types.Labels{}, err + } log.Debug().Msg("Got labels") @@ -125,5 +132,5 @@ func (docker *Docker) GetLabels(appId string) (types.TinyauthLabels, error) { log.Debug().Msg("No matching container found, returning empty labels") // If no matching container is found, return empty labels - return types.TinyauthLabels{}, nil + return types.Labels{}, nil } diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index f6d8a78..527ccfb 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -114,7 +114,8 @@ func (h *Handlers) AuthHandler(c *gin.Context) { // If auth is not enabled, return 200 if !authEnabled { - for key, value := range labels.Headers { + headersParsed := utils.ParseHeaders(labels.Headers) + for key, value := range headersParsed { log.Debug().Str("key", key).Str("value", value).Msg("Setting header") c.Header(key, utils.SanitizeHeader(value)) } @@ -236,7 +237,8 @@ func (h *Handlers) AuthHandler(c *gin.Context) { c.Header("Remote-Groups", utils.SanitizeHeader(userContext.OAuthGroups)) // Set the rest of the headers - for key, value := range labels.Headers { + parsedHeaders := utils.ParseHeaders(labels.Headers) + for key, value := range parsedHeaders { log.Debug().Str("key", key).Str("value", value).Msg("Setting header") c.Header(key, utils.SanitizeHeader(value)) } diff --git a/internal/types/config.go b/internal/types/config.go index 201ce57..8cf7bf9 100644 --- a/internal/types/config.go +++ b/internal/types/config.go @@ -92,3 +92,17 @@ type AuthConfig struct { type HooksConfig struct { Domain string } + +// OAuthLabels is a list of labels that can be used in a tinyauth protected container +type OAuthLabels struct { + Whitelist string + Groups string +} + +// Labels is a struct that contains the labels for a tinyauth protected container +type Labels struct { + Users string + Allowed string + Headers []string + OAuth OAuthLabels +} diff --git a/internal/types/types.go b/internal/types/types.go index d51e0d0..467b27a 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -32,15 +32,6 @@ type SessionCookie struct { OAuthGroups string } -// TinyauthLabels is the labels for the tinyauth container -type TinyauthLabels struct { - OAuthWhitelist string - Users string - Allowed string - Headers map[string]string - OAuthGroups string -} - // UserContext is the context for the user type UserContext struct { Username string diff --git a/internal/utils/utils.go b/internal/utils/utils.go index e7957c2..2c5969e 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -5,11 +5,11 @@ import ( "net/url" "os" "regexp" - "slices" "strings" - "tinyauth/internal/constants" "tinyauth/internal/types" + "github.com/traefik/paerser/parser" + "github.com/google/uuid" "github.com/rs/zerolog/log" ) @@ -174,45 +174,43 @@ func GetUsers(conf string, file string) (types.Users, error) { return ParseUsers(users) } -// Parse the docker labels to the tinyauth labels struct -func GetTinyauthLabels(labels map[string]string) types.TinyauthLabels { - // Create a new tinyauth labels struct - var tinyauthLabels types.TinyauthLabels +// Parse the headers in a map[string]string format +func ParseHeaders(headers []string) map[string]string { + // Create a map to store the headers + headerMap := make(map[string]string) - // Loop through the labels - for label, value := range labels { - - // Check if the label is in the tinyauth labels - if slices.Contains(constants.TinyauthLabels, label) { - - log.Debug().Str("label", label).Msg("Found label") - - // Add the label value to the tinyauth labels struct - switch label { - case "tinyauth.oauth.whitelist": - tinyauthLabels.OAuthWhitelist = value - case "tinyauth.users": - tinyauthLabels.Users = value - case "tinyauth.allowed": - tinyauthLabels.Allowed = value - case "tinyauth.headers": - tinyauthLabels.Headers = make(map[string]string) - headers := strings.Split(value, ",") - for _, header := range headers { - headerSplit := strings.Split(header, "=") - if len(headerSplit) != 2 { - continue - } - tinyauthLabels.Headers[headerSplit[0]] = headerSplit[1] - } - case "tinyauth.oauth.groups": - tinyauthLabels.OAuthGroups = value - } + // Loop through the headers + for _, header := range headers { + split := strings.SplitN(header, "=", 2) + if len(split) != 2 { + log.Warn().Str("header", header).Msg("Invalid header format, skipping") + continue } + key := SanitizeHeader(strings.TrimSpace(split[0])) + value := SanitizeHeader(strings.TrimSpace(split[1])) + headerMap[key] = value } - // Return the tinyauth labels - return tinyauthLabels + // Return the header map + return headerMap +} + +// Get labels parses a map of labels into a struct with only the needed labels +func GetLabels(labels map[string]string) (types.Labels, error) { + // Create a new labels struct + var labelsParsed types.Labels + + // Decode the labels into the labels struct + err := parser.Decode(labels, &labelsParsed, "tinyauth", "tinyauth.users", "tinyauth.allowed", "tinyauth.headers", "tinyauth.oauth") + + // Check if there was an error + if err != nil { + log.Error().Err(err).Msg("Error parsing labels") + return types.Labels{}, err + } + + // Return the labels struct + return labelsParsed, nil } // Check if any of the OAuth providers are configured based on the client id and secret diff --git a/internal/utils/utils_test.go b/internal/utils/utils_test.go index 42ae900..552e27f 100644 --- a/internal/utils/utils_test.go +++ b/internal/utils/utils_test.go @@ -279,29 +279,35 @@ func TestGetUsers(t *testing.T) { } } -// Test the tinyauth labels function -func TestGetTinyauthLabels(t *testing.T) { - t.Log("Testing get tinyauth labels with a valid map") +// Test the get labels function +func TestGetLabels(t *testing.T) { + t.Log("Testing get labels with a valid map") // Test the get tinyauth labels function with a valid map labels := map[string]string{ "tinyauth.users": "user1,user2", "tinyauth.oauth.whitelist": "/regex/", "tinyauth.allowed": "random", - "random": "random", "tinyauth.headers": "X-Header=value", + "tinyauth.oauth.groups": "group1,group2", } - expected := types.TinyauthLabels{ - Users: "user1,user2", - OAuthWhitelist: "/regex/", - Allowed: "random", - Headers: map[string]string{ - "X-Header": "value", + expected := types.Labels{ + Users: "user1,user2", + Allowed: "random", + Headers: []string{"X-Header=value"}, + OAuth: types.OAuthLabels{ + Whitelist: "/regex/", + Groups: "group1,group2", }, } - result := utils.GetTinyauthLabels(labels) + result, err := utils.GetLabels(labels) + + // Check if there was an error + if err != nil { + t.Fatalf("Error getting labels: %v", err) + } // Check if the result is equal to the expected if !reflect.DeepEqual(expected, result) {