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")
// Get user context
userContext := api.Hooks.UseUserContext(c)
// Check if using basic auth
_, _, 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
uri := c.Request.Header.Get("X-Forwarded-Uri")
proto := c.Request.Header.Get("X-Forwarded-Proto")
@@ -148,7 +181,7 @@ func (api *API) SetupRoutes() {
log.Debug().Msg("Authenticated")
// 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
if appAllowedErr != nil {

View File

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

View File

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

View File

@@ -2,10 +2,14 @@ package docker
import (
"context"
"strings"
appTypes "tinyauth/internal/types"
"tinyauth/internal/utils"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
apiTypes "github.com/docker/docker/api/types"
containerTypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/rs/zerolog/log"
)
func NewDocker() *Docker {
@@ -34,9 +38,9 @@ func (docker *Docker) Init() error {
return nil
}
func (docker *Docker) GetContainers() ([]types.Container, error) {
func (docker *Docker) GetContainers() ([]apiTypes.Container, error) {
// 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
if err != nil {
@@ -47,13 +51,13 @@ func (docker *Docker) GetContainers() ([]types.Container, error) {
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, err := docker.Client.ContainerInspect(docker.Context, containerId)
// Check if there was an error
if err != nil {
return types.ContainerJSON{}, err
return apiTypes.ContainerJSON{}, err
}
// Return the inspect
@@ -65,3 +69,57 @@ func (docker *Docker) DockerConnected() bool {
_, err := docker.Client.Ping(docker.Context)
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 {
OAuthWhitelist []string
Users []string
Allowed string
}
// 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, ",")
case "tinyauth.users":
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{
"tinyauth.users": "user1,user2",
"tinyauth.oauth.whitelist": "user1,user2",
"tinyauth.allowed": "random",
"random": "random",
}
expected := types.TinyauthLabels{
Users: []string{"user1", "user2"},
OAuthWhitelist: []string{"user1", "user2"},
Allowed: "random",
}
result := utils.GetTinyauthLabels(labels)