feat: allowed paths label

This commit is contained in:
Stavros
2025-02-26 19:25:54 +02:00
parent 30aab17f06
commit e4379cf3ed
7 changed files with 175 additions and 65 deletions

View File

@@ -132,12 +132,45 @@ func (api *API) SetupRoutes() {
log.Debug().Interface("proxy", proxy.Proxy).Msg("Got proxy") log.Debug().Interface("proxy", proxy.Proxy).Msg("Got proxy")
// Get user context
userContext := api.Hooks.UseUserContext(c)
// Check if using basic auth // Check if using basic auth
_, _, basicAuth := c.Request.BasicAuth() _, _, basicAuth := c.Request.BasicAuth()
// Check if auth is enabled
authEnabled, authEnabledErr := api.Auth.AuthEnabled(c)
// Handle error
if authEnabledErr != nil {
// Return 501 if nginx is the proxy or if the request is using basic auth
if proxy.Proxy == "nginx" || basicAuth {
log.Error().Err(authEnabledErr).Msg("Failed to check if auth is enabled")
c.JSON(501, gin.H{
"status": 501,
"message": "Internal Server Error",
})
return
}
// Return the internal server error page
if api.handleError(c, "Failed to check if auth is enabled", authEnabledErr) {
return
}
}
// If auth is not enabled, return 200
if !authEnabled {
// The user is allowed to access the app
c.JSON(200, gin.H{
"status": 200,
"message": "Authenticated",
})
// Stop further processing
return
}
// Get user context
userContext := api.Hooks.UseUserContext(c)
// Get headers // Get headers
uri := c.Request.Header.Get("X-Forwarded-Uri") uri := c.Request.Header.Get("X-Forwarded-Uri")
proto := c.Request.Header.Get("X-Forwarded-Proto") proto := c.Request.Header.Get("X-Forwarded-Proto")
@@ -148,7 +181,7 @@ func (api *API) SetupRoutes() {
log.Debug().Msg("Authenticated") log.Debug().Msg("Authenticated")
// Check if user is allowed to access subdomain, if request is nginx.example.com the subdomain (resource) is nginx // Check if user is allowed to access subdomain, if request is nginx.example.com the subdomain (resource) is nginx
appAllowed, appAllowedErr := api.Auth.ResourceAllowed(userContext, host) appAllowed, appAllowedErr := api.Auth.ResourceAllowed(c, userContext)
// Check if there was an error // Check if there was an error
if appAllowedErr != nil { if appAllowedErr != nil {

View File

@@ -1,12 +1,12 @@
package auth package auth
import ( import (
"regexp"
"slices" "slices"
"strings" "strings"
"time" "time"
"tinyauth/internal/docker" "tinyauth/internal/docker"
"tinyauth/internal/types" "tinyauth/internal/types"
"tinyauth/internal/utils"
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -139,76 +139,89 @@ func (auth *Auth) UserAuthConfigured() bool {
return len(auth.Users) > 0 return len(auth.Users) > 0
} }
func (auth *Auth) ResourceAllowed(context types.UserContext, host string) (bool, error) { func (auth *Auth) ResourceAllowed(c *gin.Context, context types.UserContext) (bool, error) {
// Check if we have access to the Docker API // Get headers
isConnected := auth.Docker.DockerConnected() host := c.Request.Header.Get("X-Forwarded-Host")
// If we don't have access, it is assumed that the user has access // Get app id
if !isConnected {
log.Debug().Msg("Docker not connected, allowing access")
return true, nil
}
// Get the app ID from the host
appId := strings.Split(host, ".")[0] appId := strings.Split(host, ".")[0]
// Get the containers // Check if resource is allowed
containers, containersErr := auth.Docker.GetContainers() allowed, allowedErr := auth.Docker.ContainerAction(appId, func(labels types.TinyauthLabels) (bool, error) {
// If the container has an oauth whitelist, check if the user is in it
if context.OAuth && len(labels.OAuthWhitelist) != 0 {
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 containersErr != nil { if allowedErr != nil {
return false, containersErr log.Error().Err(allowedErr).Msg("Error checking if resource is allowed")
return false, allowedErr
} }
log.Debug().Msg("Got containers") // Return if the resource is allowed
return allowed, nil
}
// Loop through the containers func (auth *Auth) AuthEnabled(c *gin.Context) (bool, error) {
for _, container := range containers { // Get headers
// Inspect the container uri := c.Request.Header.Get("X-Forwarded-Uri")
inspect, inspectErr := auth.Docker.InspectContainer(container.ID) host := c.Request.Header.Get("X-Forwarded-Host")
// If there is an error, return false // Get app id
if inspectErr != nil { appId := strings.Split(host, ".")[0]
return false, inspectErr
// Check if auth is enabled
enabled, enabledErr := auth.Docker.ContainerAction(appId, func(labels types.TinyauthLabels) (bool, error) {
// Check if the allowed label is empty
if labels.Allowed == "" {
// Auth enabled
return true, nil
} }
// Get the container name (for some reason it is /name) // Compile regex
containerName := strings.Split(inspect.Name, "/")[1] regex, regexErr := regexp.Compile(labels.Allowed)
// There is a container with the same name as the app ID // If there is an error, invalid regex, auth enabled
if containerName == appId { if regexErr != nil {
log.Debug().Str("container", containerName).Msg("Found container") log.Warn().Err(regexErr).Msg("Invalid regex")
return true, regexErr
// Get only the tinyauth labels in a struct
labels := utils.GetTinyauthLabels(inspect.Config.Labels)
log.Debug().Msg("Got labels")
// If the container has an oauth whitelist, check if the user is in it
if context.OAuth && len(labels.OAuthWhitelist) != 0 {
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
}
} }
// 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 enabledErr != nil {
log.Error().Err(enabledErr).Msg("Error checking if auth is enabled")
return true, enabledErr
} }
log.Debug().Msg("No matching container found, allowing access") return enabled, nil
// If no matching container is found, allow access
return true, nil
} }
func (auth *Auth) GetBasicAuth(c *gin.Context) *types.User { func (auth *Auth) GetBasicAuth(c *gin.Context) *types.User {

View File

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

View File

@@ -2,10 +2,14 @@ package docker
import ( import (
"context" "context"
"strings"
appTypes "tinyauth/internal/types"
"tinyauth/internal/utils"
"github.com/docker/docker/api/types" apiTypes "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container" containerTypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/rs/zerolog/log"
) )
func NewDocker() *Docker { func NewDocker() *Docker {
@@ -34,9 +38,9 @@ func (docker *Docker) Init() error {
return nil return nil
} }
func (docker *Docker) GetContainers() ([]types.Container, error) { func (docker *Docker) GetContainers() ([]apiTypes.Container, error) {
// Get the list of containers // Get the list of containers
containers, err := docker.Client.ContainerList(docker.Context, container.ListOptions{}) containers, err := docker.Client.ContainerList(docker.Context, containerTypes.ListOptions{})
// Check if there was an error // Check if there was an error
if err != nil { if err != nil {
@@ -47,13 +51,13 @@ func (docker *Docker) GetContainers() ([]types.Container, error) {
return containers, nil return containers, nil
} }
func (docker *Docker) InspectContainer(containerId string) (types.ContainerJSON, error) { func (docker *Docker) InspectContainer(containerId string) (apiTypes.ContainerJSON, error) {
// Inspect the container // Inspect the container
inspect, err := docker.Client.ContainerInspect(docker.Context, containerId) inspect, err := docker.Client.ContainerInspect(docker.Context, containerId)
// Check if there was an error // Check if there was an error
if err != nil { if err != nil {
return types.ContainerJSON{}, err return apiTypes.ContainerJSON{}, err
} }
// Return the inspect // Return the inspect
@@ -65,3 +69,57 @@ func (docker *Docker) DockerConnected() bool {
_, err := docker.Client.Ping(docker.Context) _, err := docker.Client.Ping(docker.Context)
return err == nil return err == nil
} }
func (docker *Docker) ContainerAction(appId string, run func(labels appTypes.TinyauthLabels) (bool, error)) (bool, error) {
// Check if we have access to the Docker API
isConnected := docker.DockerConnected()
// If we don't have access, it is assumed that the check passed
if !isConnected {
log.Debug().Msg("Docker not connected, passing check")
return true, nil
}
// Get the containers
containers, containersErr := docker.GetContainers()
// If there is an error, return false
if containersErr != nil {
return false, containersErr
}
log.Debug().Msg("Got containers")
// Loop through the containers
for _, container := range containers {
// Inspect the container
inspect, inspectErr := docker.InspectContainer(container.ID)
// If there is an error, return false
if inspectErr != nil {
return false, inspectErr
}
// Get the container name (for some reason it is /name)
containerName := strings.Split(inspect.Name, "/")[1]
// There is a container with the same name as the app ID
if containerName == appId {
log.Debug().Str("container", containerName).Msg("Found container")
// Get only the tinyauth labels in a struct
labels := utils.GetTinyauthLabels(inspect.Config.Labels)
log.Debug().Msg("Got labels")
// Run the function
return run(labels)
}
}
log.Debug().Msg("No matching container found, allowing access")
// If no matching container is found, allow access
return true, nil
}

View File

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

View File

@@ -195,6 +195,8 @@ func GetTinyauthLabels(labels map[string]string) types.TinyauthLabels {
tinyauthLabels.OAuthWhitelist = strings.Split(value, ",") tinyauthLabels.OAuthWhitelist = strings.Split(value, ",")
case "tinyauth.users": case "tinyauth.users":
tinyauthLabels.Users = strings.Split(value, ",") tinyauthLabels.Users = strings.Split(value, ",")
case "tinyauth.allowed":
tinyauthLabels.Allowed = value
} }
} }
} }

View File

@@ -298,12 +298,14 @@ func TestGetTinyauthLabels(t *testing.T) {
labels := map[string]string{ labels := map[string]string{
"tinyauth.users": "user1,user2", "tinyauth.users": "user1,user2",
"tinyauth.oauth.whitelist": "user1,user2", "tinyauth.oauth.whitelist": "user1,user2",
"tinyauth.allowed": "random",
"random": "random", "random": "random",
} }
expected := types.TinyauthLabels{ expected := types.TinyauthLabels{
Users: []string{"user1", "user2"}, Users: []string{"user1", "user2"},
OAuthWhitelist: []string{"user1", "user2"}, OAuthWhitelist: []string{"user1", "user2"},
Allowed: "random",
} }
result := utils.GetTinyauthLabels(labels) result := utils.GetTinyauthLabels(labels)