refactor: move to traefik paerser for label parsing (#197)

* refactor: move to traefik paerser for label parsing

* fix: sanitize headers before adding to map

* refactor: use splitn in header parser

* refactor: ignore containers that failed to get inspected in docker
This commit is contained in:
Stavros
2025-06-15 19:58:23 +03:00
committed by GitHub
parent ee83c177f4
commit 3397e2aa8e
12 changed files with 95 additions and 83 deletions

View File

@@ -1,5 +1,5 @@
# Site builder # Site builder
FROM oven/bun:1.2.15-alpine AS frontend-builder FROM oven/bun:1.2.16-alpine AS frontend-builder
WORKDIR /frontend WORKDIR /frontend

View File

@@ -1,4 +1,4 @@
FROM oven/bun:1.1.45-alpine FROM oven/bun:1.2.16-alpine
WORKDIR /frontend WORKDIR /frontend

1
go.mod
View File

@@ -24,6 +24,7 @@ require (
github.com/moby/sys/atomicwriter v0.1.0 // indirect github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect github.com/moby/term v0.5.2 // indirect
github.com/morikuni/aec v1.0.0 // 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 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect

2
go.sum
View File

@@ -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/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 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 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 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 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= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=

View File

@@ -264,11 +264,11 @@ func (auth *Auth) UserAuthConfigured() bool {
return len(auth.Config.Users) > 0 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 // Check if oauth is allowed
if context.OAuth { if context.OAuth {
log.Debug().Msg("Checking OAuth whitelist") log.Debug().Msg("Checking OAuth whitelist")
return utils.CheckWhitelist(labels.OAuthWhitelist, context.Email) return utils.CheckWhitelist(labels.OAuth.Whitelist, context.Email)
} }
// Check users // Check users
@@ -277,9 +277,9 @@ func (auth *Auth) ResourceAllowed(c *gin.Context, context types.UserContext, lab
return utils.CheckWhitelist(labels.Users, context.Username) 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 // Check if groups are required
if labels.OAuthGroups == "" { if labels.OAuth.Groups == "" {
return true 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 every group check if it is in the required groups
for _, group := range oauthGroups { 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") log.Debug().Str("group", group).Msg("Group is in required groups")
return true return true
} }
@@ -307,7 +307,7 @@ func (auth *Auth) OAuthGroup(c *gin.Context, context types.UserContext, labels t
return false 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 // Get headers
uri := c.Request.Header.Get("X-Forwarded-Uri") uri := c.Request.Header.Get("X-Forwarded-Uri")

View File

@@ -1,14 +1,5 @@
package constants 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) // Claims are the OIDC supported claims (including preferd username for some reason)
type Claims struct { type Claims struct {
Name string `json:"name"` Name string `json:"name"`

View File

@@ -74,14 +74,14 @@ func (docker *Docker) DockerConnected() bool {
return err == nil 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 // Check if we have access to the Docker API
isConnected := docker.DockerConnected() isConnected := docker.DockerConnected()
// If we don't have access, return an empty struct // If we don't have access, return an empty struct
if !isConnected { if !isConnected {
log.Debug().Msg("Docker not connected, returning empty labels") log.Debug().Msg("Docker not connected, returning empty labels")
return types.TinyauthLabels{}, nil return types.Labels{}, nil
} }
// Get the containers // Get the containers
@@ -89,7 +89,7 @@ func (docker *Docker) GetLabels(appId string) (types.TinyauthLabels, error) {
// If there is an error, return false // If there is an error, return false
if err != nil { if err != nil {
return types.TinyauthLabels{}, err return types.Labels{}, err
} }
log.Debug().Msg("Got containers") log.Debug().Msg("Got containers")
@@ -99,9 +99,10 @@ func (docker *Docker) GetLabels(appId string) (types.TinyauthLabels, error) {
// Inspect the container // Inspect the container
inspect, err := docker.InspectContainer(container.ID) inspect, err := docker.InspectContainer(container.ID)
// If there is an error, return false // Check if there was an error
if err != nil { 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) // 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") log.Debug().Str("container", containerName).Msg("Found container")
// Get only the tinyauth labels in a struct // 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") 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") log.Debug().Msg("No matching container found, returning empty labels")
// If no matching container is found, return empty labels // If no matching container is found, return empty labels
return types.TinyauthLabels{}, nil return types.Labels{}, nil
} }

View File

@@ -114,7 +114,8 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
// If auth is not enabled, return 200 // If auth is not enabled, return 200
if !authEnabled { 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") log.Debug().Str("key", key).Str("value", value).Msg("Setting header")
c.Header(key, utils.SanitizeHeader(value)) 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)) c.Header("Remote-Groups", utils.SanitizeHeader(userContext.OAuthGroups))
// Set the rest of the headers // 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") log.Debug().Str("key", key).Str("value", value).Msg("Setting header")
c.Header(key, utils.SanitizeHeader(value)) c.Header(key, utils.SanitizeHeader(value))
} }

View File

@@ -92,3 +92,17 @@ type AuthConfig struct {
type HooksConfig struct { type HooksConfig struct {
Domain string 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
}

View File

@@ -32,15 +32,6 @@ type SessionCookie struct {
OAuthGroups string 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 // UserContext is the context for the user
type UserContext struct { type UserContext struct {
Username string Username string

View File

@@ -5,11 +5,11 @@ import (
"net/url" "net/url"
"os" "os"
"regexp" "regexp"
"slices"
"strings" "strings"
"tinyauth/internal/constants"
"tinyauth/internal/types" "tinyauth/internal/types"
"github.com/traefik/paerser/parser"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@@ -174,45 +174,43 @@ func GetUsers(conf string, file string) (types.Users, error) {
return ParseUsers(users) return ParseUsers(users)
} }
// Parse the docker labels to the tinyauth labels struct // Parse the headers in a map[string]string format
func GetTinyauthLabels(labels map[string]string) types.TinyauthLabels { func ParseHeaders(headers []string) map[string]string {
// Create a new tinyauth labels struct // Create a map to store the headers
var tinyauthLabels types.TinyauthLabels headerMap := make(map[string]string)
// Loop through the labels // Loop through the headers
for label, value := range labels { for _, header := range headers {
split := strings.SplitN(header, "=", 2)
// Check if the label is in the tinyauth labels if len(split) != 2 {
if slices.Contains(constants.TinyauthLabels, label) { log.Warn().Str("header", header).Msg("Invalid header format, skipping")
continue
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
}
} }
key := SanitizeHeader(strings.TrimSpace(split[0]))
value := SanitizeHeader(strings.TrimSpace(split[1]))
headerMap[key] = value
} }
// Return the tinyauth labels // Return the header map
return tinyauthLabels 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 // Check if any of the OAuth providers are configured based on the client id and secret

View File

@@ -279,29 +279,35 @@ func TestGetUsers(t *testing.T) {
} }
} }
// Test the tinyauth labels function // Test the get labels function
func TestGetTinyauthLabels(t *testing.T) { func TestGetLabels(t *testing.T) {
t.Log("Testing get tinyauth labels with a valid map") t.Log("Testing get labels with a valid map")
// Test the get tinyauth labels function with a valid map // Test the get tinyauth labels function with a valid map
labels := map[string]string{ labels := map[string]string{
"tinyauth.users": "user1,user2", "tinyauth.users": "user1,user2",
"tinyauth.oauth.whitelist": "/regex/", "tinyauth.oauth.whitelist": "/regex/",
"tinyauth.allowed": "random", "tinyauth.allowed": "random",
"random": "random",
"tinyauth.headers": "X-Header=value", "tinyauth.headers": "X-Header=value",
"tinyauth.oauth.groups": "group1,group2",
} }
expected := types.TinyauthLabels{ expected := types.Labels{
Users: "user1,user2", Users: "user1,user2",
OAuthWhitelist: "/regex/", Allowed: "random",
Allowed: "random", Headers: []string{"X-Header=value"},
Headers: map[string]string{ OAuth: types.OAuthLabels{
"X-Header": "value", 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 // Check if the result is equal to the expected
if !reflect.DeepEqual(expected, result) { if !reflect.DeepEqual(expected, result) {