feat: add ability to set custom headers

This commit is contained in:
Stavros
2025-03-26 18:05:43 +02:00
parent 46526b564e
commit 14ce8ecf98
10 changed files with 119 additions and 84 deletions

View File

@@ -133,7 +133,7 @@ var rootCmd = &cobra.Command{
hooks := hooks.NewHooks(auth, providers) hooks := hooks.NewHooks(auth, providers)
// Create handlers // Create handlers
handlers := handlers.NewHandlers(serverConfig, auth, hooks, providers) handlers := handlers.NewHandlers(serverConfig, auth, hooks, providers, docker)
// Create API // Create API
api := api.NewAPI(apiConfig, handlers) api := api.NewAPI(apiConfig, handlers)

View File

@@ -14,7 +14,6 @@ services:
labels: labels:
traefik.enable: true traefik.enable: true
traefik.http.routers.nginx.rule: Host(`whoami.example.com`) traefik.http.routers.nginx.rule: Host(`whoami.example.com`)
traefik.http.services.nginx.loadbalancer.server.port: 80
traefik.http.routers.nginx.middlewares: tinyauth traefik.http.routers.nginx.middlewares: tinyauth
tinyauth-frontend: tinyauth-frontend:
@@ -29,7 +28,6 @@ services:
labels: labels:
traefik.enable: true traefik.enable: true
traefik.http.routers.tinyauth.rule: Host(`tinyauth.example.com`) traefik.http.routers.tinyauth.rule: Host(`tinyauth.example.com`)
traefik.http.services.tinyauth.loadbalancer.server.port: 5173
tinyauth-backend: tinyauth-backend:
container_name: tinyauth-backend container_name: tinyauth-backend
@@ -41,6 +39,7 @@ services:
- ./internal:/tinyauth/internal - ./internal:/tinyauth/internal
- ./cmd:/tinyauth/cmd - ./cmd:/tinyauth/cmd
- ./main.go:/tinyauth/main.go - ./main.go:/tinyauth/main.go
- /var/run/docker.sock:/var/run/docker.sock
ports: ports:
- 3000:3000 - 3000:3000
labels: labels:

View File

@@ -14,7 +14,6 @@ services:
labels: labels:
traefik.enable: true traefik.enable: true
traefik.http.routers.nginx.rule: Host(`whoami.example.com`) traefik.http.routers.nginx.rule: Host(`whoami.example.com`)
traefik.http.services.nginx.loadbalancer.server.port: 80
traefik.http.routers.nginx.middlewares: tinyauth traefik.http.routers.nginx.middlewares: tinyauth
tinyauth: tinyauth:
@@ -27,5 +26,4 @@ services:
labels: labels:
traefik.enable: true traefik.enable: true
traefik.http.routers.tinyauth.rule: Host(`tinyauth.example.com`) traefik.http.routers.tinyauth.rule: Host(`tinyauth.example.com`)
traefik.http.services.tinyauth.loadbalancer.server.port: 3000
traefik.http.middlewares.tinyauth.forwardauth.address: http://tinyauth:3000/api/auth/traefik traefik.http.middlewares.tinyauth.forwardauth.address: http://tinyauth:3000/api/auth/traefik

View File

@@ -78,7 +78,7 @@ func getAPI(t *testing.T) *api.API {
hooks := hooks.NewHooks(auth, providers) hooks := hooks.NewHooks(auth, providers)
// Create handlers service // Create handlers service
handlers := handlers.NewHandlers(handlersConfig, auth, hooks, providers) handlers := handlers.NewHandlers(handlersConfig, auth, hooks, providers, docker)
// Create API // Create API
api := api.NewAPI(apiConfig, handlers) api := api.NewAPI(apiConfig, handlers)

View File

@@ -159,41 +159,35 @@ func (auth *Auth) ResourceAllowed(c *gin.Context, context types.UserContext) (bo
// Get app id // Get app id
appId := strings.Split(host, ".")[0] appId := strings.Split(host, ".")[0]
// Check if resource is allowed // Get the container labels
allowed, err := auth.Docker.ContainerAction(appId, func(labels types.TinyauthLabels) (bool, error) { labels, err := auth.Docker.GetLabels(appId)
// If the container has an oauth whitelist, check if the user is in it
if context.OAuth {
if len(labels.OAuthWhitelist) == 0 {
return true, nil
}
log.Debug().Msg("Checking OAuth whitelist")
if slices.Contains(labels.OAuthWhitelist, context.Username) {
return true, nil
}
return false, nil
}
// If the container has users, check if the user is in it
if len(labels.Users) != 0 {
log.Debug().Msg("Checking users")
if slices.Contains(labels.Users, context.Username) {
return true, nil
}
return false, nil
}
// Allowed
return true, nil
})
// If there is an error, return false // If there is an error, return false
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error checking if resource is allowed")
return false, err return false, err
} }
// Return if the resource is allowed // Check if oauth is allowed
return allowed, nil if context.OAuth {
if len(labels.OAuthWhitelist) == 0 {
return true, nil
}
log.Debug().Msg("Checking OAuth whitelist")
if slices.Contains(labels.OAuthWhitelist, context.Username) {
return true, nil
}
}
// Check if user is allowed
if len(labels.Users) != 0 {
log.Debug().Msg("Checking users")
if slices.Contains(labels.Users, context.Username) {
return true, nil
}
}
// Not allowed
return false, nil
} }
func (auth *Auth) AuthEnabled(c *gin.Context) (bool, error) { func (auth *Auth) AuthEnabled(c *gin.Context) (bool, error) {
@@ -204,40 +198,37 @@ func (auth *Auth) AuthEnabled(c *gin.Context) (bool, error) {
// Get app id // Get app id
appId := strings.Split(host, ".")[0] appId := strings.Split(host, ".")[0]
// Check if auth is enabled // Get the container labels
enabled, err := auth.Docker.ContainerAction(appId, func(labels types.TinyauthLabels) (bool, error) { labels, err := auth.Docker.GetLabels(appId)
// Check if the allowed label is empty
if labels.Allowed == "" {
// Auth enabled
return true, nil
}
// Compile regex
regex, err := regexp.Compile(labels.Allowed)
// If there is an error, invalid regex, auth enabled
if err != nil {
log.Warn().Err(err).Msg("Invalid regex")
return true, err
}
// Check if the uri matches the regex
if regex.MatchString(uri) {
// Auth disabled
return false, nil
}
// Auth enabled
return true, nil
})
// If there is an error, auth enabled // If there is an error, auth enabled
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error checking if auth is enabled")
return true, err return true, err
} }
return enabled, nil // Check if the allowed label is empty
if labels.Allowed == "" {
// Auth enabled
return true, nil
}
// Compile regex
regex, err := regexp.Compile(labels.Allowed)
// If there is an error, invalid regex, auth enabled
if err != nil {
log.Warn().Err(err).Msg("Invalid regex")
return true, err
}
// Check if the uri matches the regex
if regex.MatchString(uri) {
// Auth disabled
return false, nil
}
// Auth enabled
return true, nil
} }
func (auth *Auth) GetBasicAuth(c *gin.Context) *types.User { func (auth *Auth) GetBasicAuth(c *gin.Context) *types.User {

View File

@@ -5,4 +5,5 @@ var TinyauthLabels = []string{
"tinyauth.oauth.whitelist", "tinyauth.oauth.whitelist",
"tinyauth.users", "tinyauth.users",
"tinyauth.allowed", "tinyauth.allowed",
"tinyauth.headers",
} }

View File

@@ -3,7 +3,7 @@ package docker
import ( import (
"context" "context"
"strings" "strings"
appTypes "tinyauth/internal/types" "tinyauth/internal/types"
"tinyauth/internal/utils" "tinyauth/internal/utils"
apiTypes "github.com/docker/docker/api/types" apiTypes "github.com/docker/docker/api/types"
@@ -70,14 +70,14 @@ func (docker *Docker) DockerConnected() bool {
return err == nil return err == nil
} }
func (docker *Docker) ContainerAction(appId string, runCheck func(labels appTypes.TinyauthLabels) (bool, error)) (bool, error) { func (docker *Docker) GetLabels(appId string) (types.TinyauthLabels, 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, it is assumed that the check passed // If we don't have access, return an empty struct
if !isConnected { if !isConnected {
log.Debug().Msg("Docker not connected, passing check") log.Debug().Msg("Docker not connected, returning empty labels")
return true, nil return types.TinyauthLabels{}, nil
} }
// Get the containers // Get the containers
@@ -85,7 +85,7 @@ func (docker *Docker) ContainerAction(appId string, runCheck func(labels appType
// If there is an error, return false // If there is an error, return false
if err != nil { if err != nil {
return false, err return types.TinyauthLabels{}, err
} }
log.Debug().Msg("Got containers") log.Debug().Msg("Got containers")
@@ -97,11 +97,11 @@ func (docker *Docker) ContainerAction(appId string, runCheck func(labels appType
// If there is an error, return false // If there is an error, return false
if err != nil { if err != nil {
return false, err return types.TinyauthLabels{}, err
} }
// Get the container name (for some reason it is /name) // Get the container name (for some reason it is /name)
containerName := strings.Split(inspect.Name, "/")[1] containerName := strings.TrimPrefix(inspect.Name, "/")
// There is a container with the same name as the app ID // There is a container with the same name as the app ID
if containerName == appId { if containerName == appId {
@@ -112,14 +112,14 @@ func (docker *Docker) ContainerAction(appId string, runCheck func(labels appType
log.Debug().Msg("Got labels") log.Debug().Msg("Got labels")
// Run the check // Return labels
return runCheck(labels) return labels, nil
} }
} }
log.Debug().Msg("No matching container found, passing check") log.Debug().Msg("No matching container found, returning empty labels")
// If no matching container is found, pass check // If no matching container is found, return empty labels
return true, nil return types.TinyauthLabels{}, nil
} }

View File

@@ -6,6 +6,7 @@ import (
"net/http" "net/http"
"strings" "strings"
"tinyauth/internal/auth" "tinyauth/internal/auth"
"tinyauth/internal/docker"
"tinyauth/internal/hooks" "tinyauth/internal/hooks"
"tinyauth/internal/providers" "tinyauth/internal/providers"
"tinyauth/internal/types" "tinyauth/internal/types"
@@ -16,12 +17,13 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
func NewHandlers(config types.HandlersConfig, auth *auth.Auth, hooks *hooks.Hooks, providers *providers.Providers) *Handlers { func NewHandlers(config types.HandlersConfig, auth *auth.Auth, hooks *hooks.Hooks, providers *providers.Providers, docker *docker.Docker) *Handlers {
return &Handlers{ return &Handlers{
Config: config, Config: config,
Auth: auth, Auth: auth,
Hooks: hooks, Hooks: hooks,
Providers: providers, Providers: providers,
Docker: docker,
} }
} }
@@ -30,6 +32,7 @@ type Handlers struct {
Auth *auth.Auth Auth *auth.Auth
Hooks *hooks.Hooks Hooks *hooks.Hooks
Providers *providers.Providers Providers *providers.Providers
Docker *docker.Docker
} }
func (h *Handlers) AuthHandler(c *gin.Context) { func (h *Handlers) AuthHandler(c *gin.Context) {
@@ -60,12 +63,39 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
log.Debug().Interface("proxy", proxy.Proxy).Msg("Got proxy") log.Debug().Interface("proxy", proxy.Proxy).Msg("Got proxy")
// Get headers
uri := c.Request.Header.Get("X-Forwarded-Uri")
proto := c.Request.Header.Get("X-Forwarded-Proto")
host := c.Request.Header.Get("X-Forwarded-Host")
// Check if auth is enabled // Check if auth is enabled
authEnabled, err := h.Auth.AuthEnabled(c) authEnabled, err := h.Auth.AuthEnabled(c)
// Handle error // Check if there was an error
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to check if auth is enabled") log.Error().Err(err).Msg("Failed to check if app is allowed")
if proxy.Proxy == "nginx" || !isBrowser {
c.JSON(500, gin.H{
"status": 500,
"message": "Internal Server Error",
})
return
}
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
return
}
// Get the app id
appId := strings.Split(host, ".")[0]
// Get the container labels
labels, err := h.Docker.GetLabels(appId)
// Check if there was an error
if err != nil {
log.Error().Err(err).Msg("Failed to check if app is allowed")
if proxy.Proxy == "nginx" || !isBrowser { if proxy.Proxy == "nginx" || !isBrowser {
c.JSON(500, gin.H{ c.JSON(500, gin.H{
@@ -81,6 +111,10 @@ 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 {
log.Debug().Str("key", key).Str("value", value).Msg("Setting header")
c.Header(key, value)
}
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"status": 200, "status": 200,
"message": "Authenticated", "message": "Authenticated",
@@ -91,11 +125,6 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
// Get user context // Get user context
userContext := h.Hooks.UseUserContext(c) userContext := h.Hooks.UseUserContext(c)
// Get headers
uri := c.Request.Header.Get("X-Forwarded-Uri")
proto := c.Request.Header.Get("X-Forwarded-Proto")
host := c.Request.Header.Get("X-Forwarded-Host")
// Check if user is logged in // Check if user is logged in
if userContext.IsLoggedIn { if userContext.IsLoggedIn {
log.Debug().Msg("Authenticated") log.Debug().Msg("Authenticated")
@@ -157,6 +186,12 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
// Set the user header // Set the user header
c.Header("Remote-User", userContext.Username) c.Header("Remote-User", userContext.Username)
// Set the rest of the headers
for key, value := range labels.Headers {
log.Debug().Str("key", key).Str("value", value).Msg("Setting header")
c.Header(key, value)
}
// The user is allowed to access the app // The user is allowed to access the app
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"status": 200, "status": 200,

View File

@@ -124,6 +124,7 @@ type TinyauthLabels struct {
OAuthWhitelist []string OAuthWhitelist []string
Users []string Users []string
Allowed string Allowed string
Headers map[string]string
} }
// TailscaleQuery is the query parameters for the tailscale endpoint // TailscaleQuery is the query parameters for the tailscale endpoint

View File

@@ -193,6 +193,16 @@ func GetTinyauthLabels(labels map[string]string) types.TinyauthLabels {
tinyauthLabels.Users = strings.Split(value, ",") tinyauthLabels.Users = strings.Split(value, ",")
case "tinyauth.allowed": case "tinyauth.allowed":
tinyauthLabels.Allowed = value 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]
}
} }
} }
} }