mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-10-30 05:35:44 +00:00 
			
		
		
		
	Compare commits
	
		
			3 Commits
		
	
	
		
			30fe695371
			...
			feat/ip-al
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 5dda1d22e7 | ||
|   | 1770eb3e8e | ||
|   | fae5e7919a | 
							
								
								
									
										2
									
								
								.github/workflows/nightly.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/nightly.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,6 +1,8 @@ | ||||
| name: Nightly Release | ||||
| on: | ||||
|   workflow_dispatch: | ||||
|   schedule: | ||||
|     - cron: "0 0 * * *" | ||||
|  | ||||
| jobs: | ||||
|   create-release: | ||||
|   | ||||
| @@ -42,6 +42,7 @@ | ||||
|     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||
|     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", | ||||
|     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", | ||||
|     "unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||
|     "unauthorizedButton": "Try again", | ||||
|     "untrustedRedirectTitle": "Untrusted redirect", | ||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?", | ||||
|   | ||||
| @@ -42,6 +42,7 @@ | ||||
|     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||
|     "unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.", | ||||
|     "unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.", | ||||
|     "unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.", | ||||
|     "unauthorizedButton": "Try again", | ||||
|     "untrustedRedirectTitle": "Untrusted redirect", | ||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?", | ||||
|   | ||||
| @@ -17,8 +17,9 @@ 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) { | ||||
|   if (!username && !ip) { | ||||
|     return <Navigate to="/" />; | ||||
|   } | ||||
|  | ||||
| @@ -41,6 +42,10 @@ export const UnauthorizedPage = () => { | ||||
|     i18nKey = "unauthorizedGroupsSubtitle"; | ||||
|   } | ||||
|  | ||||
|   if (ip) { | ||||
|     i18nKey = "unauthorizedIpSubtitle"; | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <Card className="min-w-xs sm:min-w-sm"> | ||||
|       <CardHeader> | ||||
| @@ -55,6 +60,7 @@ export const UnauthorizedPage = () => { | ||||
|             values={{ | ||||
|               username, | ||||
|               resource, | ||||
|               ip, | ||||
|             }} | ||||
|           /> | ||||
|         </CardDescription> | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user