diff --git a/internal/api/api.go b/internal/api/api.go index b95113d..fa0bb7b 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -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 { diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 1566b3f..926ee2a 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -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 { diff --git a/internal/constants/constants.go b/internal/constants/constants.go index ce12089..e515d6c 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -4,4 +4,5 @@ package constants var TinyauthLabels = []string{ "tinyauth.oauth.whitelist", "tinyauth.users", + "tinyauth.allowed", } diff --git a/internal/docker/docker.go b/internal/docker/docker.go index 3accea6..0788f96 100644 --- a/internal/docker/docker.go +++ b/internal/docker/docker.go @@ -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 +} diff --git a/internal/types/types.go b/internal/types/types.go index 7a4f7d8..d8bdfbb 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -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 diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 4d0ebab..89e26eb 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -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 } } } diff --git a/internal/utils/utils_test.go b/internal/utils/utils_test.go index d2e475b..36a1c1b 100644 --- a/internal/utils/utils_test.go +++ b/internal/utils/utils_test.go @@ -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)