diff --git a/frontend/src/lib/i18n/locales/en-US.json b/frontend/src/lib/i18n/locales/en-US.json index e0ca837..0eae0bc 100644 --- a/frontend/src/lib/i18n/locales/en-US.json +++ b/frontend/src/lib/i18n/locales/en-US.json @@ -42,6 +42,7 @@ "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource {{resource}}.", "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", + "unauthorizedIpSubtitle": "Your IP address {{ip}} is not authorized to access the resource {{resource}}.", "unauthorizedButton": "Try again", "untrustedRedirectTitle": "Untrusted redirect", "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", diff --git a/frontend/src/lib/i18n/locales/en.json b/frontend/src/lib/i18n/locales/en.json index e0ca837..0eae0bc 100644 --- a/frontend/src/lib/i18n/locales/en.json +++ b/frontend/src/lib/i18n/locales/en.json @@ -42,6 +42,7 @@ "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource {{resource}}.", "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", + "unauthorizedIpSubtitle": "Your IP address {{ip}} is not authorized to access the resource {{resource}}.", "unauthorizedButton": "Try again", "untrustedRedirectTitle": "Untrusted redirect", "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", diff --git a/frontend/src/pages/unauthorized-page.tsx b/frontend/src/pages/unauthorized-page.tsx index e41cad8..25625f1 100644 --- a/frontend/src/pages/unauthorized-page.tsx +++ b/frontend/src/pages/unauthorized-page.tsx @@ -17,9 +17,10 @@ export const UnauthorizedPage = () => { const username = searchParams.get("username"); const resource = searchParams.get("resource"); const groupErr = searchParams.get("groupErr"); + const ip = searchParams.get("ip"); - if (!username) { - return ; + if (!username && !ip) { + return ; } const { t } = useTranslation(); @@ -41,6 +42,10 @@ export const UnauthorizedPage = () => { i18nKey = "unauthorizedGroupsSubtitle"; } + if (ip) { + i18nKey = "unauthorizedIpSubtitle"; + } + return ( @@ -55,6 +60,7 @@ export const UnauthorizedPage = () => { values={{ username, resource, + ip, }} /> diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 9f09d24..55a6622 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -351,3 +351,44 @@ func (auth *Auth) GetBasicAuth(c *gin.Context) *types.User { Password: password, } } + +func (auth *Auth) CheckIP(c *gin.Context, labels types.Labels) bool { + // Get the IP address from the request + ip := c.ClientIP() + + // Check if the IP is in block list + for _, blocked := range labels.IP.Block { + res, err := utils.FilterIP(blocked, ip) + if err != nil { + log.Warn().Err(err).Str("item", blocked).Msg("Invalid IP/CIDR in block list") + continue + } + if res { + log.Warn().Str("ip", ip).Str("item", blocked).Msg("IP is in blocked list, denying access") + return false + } + } + + // For every IP in the allow list, check if the IP matches + for _, allowed := range labels.IP.Allow { + res, err := utils.FilterIP(allowed, ip) + if err != nil { + log.Warn().Err(err).Str("item", allowed).Msg("Invalid IP/CIDR in allow list") + continue + } + if res { + log.Debug().Str("ip", ip).Str("item", allowed).Msg("IP is in allowed list, allowing access") + return true + } + } + + // If not in allowed range and allowed range is not empty, deny access + if len(labels.IP.Allow) > 0 { + log.Warn().Str("ip", ip).Msg("IP not in allow list, denying access") + return false + } + + log.Debug().Str("ip", ip).Msg("IP not in allow or block list, allowing by default") + + return true +} diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 5ba49e0..aac5f17 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -96,6 +96,38 @@ func (h *Handlers) AuthHandler(c *gin.Context) { return } + // Check if the IP is allowed/blocked + ip := c.ClientIP() + if !h.Auth.CheckIP(c, labels) { + log.Warn().Str("ip", ip).Msg("IP not allowed") + + if proxy.Proxy == "nginx" || !isBrowser { + c.JSON(403, gin.H{ + "status": 403, + "message": "Forbidden", + }) + return + } + + values := types.UnauthorizedQuery{ + Resource: strings.Split(host, ".")[0], + IP: ip, + } + + // Build query + queries, err := query.Values(values) + + // Handle error + if err != nil { + log.Error().Err(err).Msg("Failed to build queries") + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", h.Config.AppURL)) + return + } + + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", h.Config.AppURL, queries.Encode())) + return + } + // Check if auth is enabled authEnabled, err := h.Auth.AuthEnabled(c, labels) diff --git a/internal/types/api.go b/internal/types/api.go index 8e5d73a..fbf8bf7 100644 --- a/internal/types/api.go +++ b/internal/types/api.go @@ -21,6 +21,7 @@ type UnauthorizedQuery struct { Username string `url:"username"` Resource string `url:"resource"` GroupErr bool `url:"groupErr"` + IP string `url:"ip"` } // Proxy is the uri parameters for the proxy endpoint diff --git a/internal/types/config.go b/internal/types/config.go index 80c6805..edb7123 100644 --- a/internal/types/config.go +++ b/internal/types/config.go @@ -105,6 +105,12 @@ type BasicLabels struct { Password string } +// IP labels for a tinyauth protected container +type IPLabels struct { + Allow []string + Block []string +} + // Labels is a struct that contains the labels for a tinyauth protected container type Labels struct { Users string @@ -113,4 +119,5 @@ type Labels struct { Domain string Basic BasicLabels OAuth OAuthLabels + IP IPLabels } diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 39bd762..8209802 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -3,6 +3,7 @@ package utils import ( "encoding/base64" "errors" + "net" "net/url" "os" "regexp" @@ -202,7 +203,7 @@ func GetLabels(labels map[string]string) (types.Labels, error) { var labelsParsed types.Labels // Decode the labels into the labels struct - err := parser.Decode(labels, &labelsParsed, "tinyauth", "tinyauth.users", "tinyauth.allowed", "tinyauth.headers", "tinyauth.domain", "tinyauth.basic", "tinyauth.oauth") + err := parser.Decode(labels, &labelsParsed, "tinyauth", "tinyauth.users", "tinyauth.allowed", "tinyauth.headers", "tinyauth.domain", "tinyauth.basic", "tinyauth.oauth", "tinyauth.ip") // Check if there was an error if err != nil { @@ -368,3 +369,39 @@ func GetBasicAuth(username string, password string) string { // Encode the auth string to base64 return base64.StdEncoding.EncodeToString([]byte(auth)) } + +// Check if an IP is contained in a CIDR range/matches a single IP +func FilterIP(filter string, ip string) (bool, error) { + // Convert the check IP to an IP instance + ipAddr := net.ParseIP(ip) + + // Check if the filter is a CIDR range + if strings.Contains(filter, "/") { + // Parse the CIDR range + _, cidr, err := net.ParseCIDR(filter) + + // Check if there was an error + if err != nil { + return false, err + } + + // Check if the IP is in the CIDR range + return cidr.Contains(ipAddr), nil + } + + // Parse the filter as a single IP + ipFilter := net.ParseIP(filter) + + // Check if the IP is valid + if ipFilter == nil { + return false, errors.New("invalid IP address in filter") + } + + // Check if the IP matches the filter + if ipFilter.Equal(ipAddr) { + return true, nil + } + + // If the filter is not a CIDR range or a single IP, return false + return false, nil +}