mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-10-31 14:15:50 +00:00 
			
		
		
		
	Compare commits
	
		
			3 Commits
		
	
	
		
			fc7e395e66
			...
			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 | name: Nightly Release | ||||||
| on: | on: | ||||||
|   workflow_dispatch: |   workflow_dispatch: | ||||||
|  |   schedule: | ||||||
|  |     - cron: "0 0 * * *" | ||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
|   create-release: |   create-release: | ||||||
|   | |||||||
| @@ -42,6 +42,7 @@ | |||||||
|     "unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.", |     "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.", |     "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>.", |     "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", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "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?", |     "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>.", |     "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.", |     "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>.", |     "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", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "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?", |     "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 username = searchParams.get("username"); | ||||||
|   const resource = searchParams.get("resource"); |   const resource = searchParams.get("resource"); | ||||||
|   const groupErr = searchParams.get("groupErr"); |   const groupErr = searchParams.get("groupErr"); | ||||||
|  |   const ip = searchParams.get("ip"); | ||||||
|  |  | ||||||
|   if (!username) { |   if (!username && !ip) { | ||||||
|     return <Navigate to="/" />; |     return <Navigate to="/" />; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -41,6 +42,10 @@ export const UnauthorizedPage = () => { | |||||||
|     i18nKey = "unauthorizedGroupsSubtitle"; |     i18nKey = "unauthorizedGroupsSubtitle"; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   if (ip) { | ||||||
|  |     i18nKey = "unauthorizedIpSubtitle"; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Card className="min-w-xs sm:min-w-sm"> |     <Card className="min-w-xs sm:min-w-sm"> | ||||||
|       <CardHeader> |       <CardHeader> | ||||||
| @@ -55,6 +60,7 @@ export const UnauthorizedPage = () => { | |||||||
|             values={{ |             values={{ | ||||||
|               username, |               username, | ||||||
|               resource, |               resource, | ||||||
|  |               ip, | ||||||
|             }} |             }} | ||||||
|           /> |           /> | ||||||
|         </CardDescription> |         </CardDescription> | ||||||
|   | |||||||
| @@ -351,3 +351,44 @@ func (auth *Auth) GetBasicAuth(c *gin.Context) *types.User { | |||||||
| 		Password: password, | 		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 | 		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 | 	// Check if auth is enabled | ||||||
| 	authEnabled, err := h.Auth.AuthEnabled(c, labels) | 	authEnabled, err := h.Auth.AuthEnabled(c, labels) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -21,6 +21,7 @@ type UnauthorizedQuery struct { | |||||||
| 	Username string `url:"username"` | 	Username string `url:"username"` | ||||||
| 	Resource string `url:"resource"` | 	Resource string `url:"resource"` | ||||||
| 	GroupErr bool   `url:"groupErr"` | 	GroupErr bool   `url:"groupErr"` | ||||||
|  | 	IP       string `url:"ip"` | ||||||
| } | } | ||||||
|  |  | ||||||
| // Proxy is the uri parameters for the proxy endpoint | // Proxy is the uri parameters for the proxy endpoint | ||||||
|   | |||||||
| @@ -105,6 +105,12 @@ type BasicLabels struct { | |||||||
| 	Password string | 	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 | // Labels is a struct that contains the labels for a tinyauth protected container | ||||||
| type Labels struct { | type Labels struct { | ||||||
| 	Users   string | 	Users   string | ||||||
| @@ -113,4 +119,5 @@ type Labels struct { | |||||||
| 	Domain  string | 	Domain  string | ||||||
| 	Basic   BasicLabels | 	Basic   BasicLabels | ||||||
| 	OAuth   OAuthLabels | 	OAuth   OAuthLabels | ||||||
|  | 	IP      IPLabels | ||||||
| } | } | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ package utils | |||||||
| import ( | import ( | ||||||
| 	"encoding/base64" | 	"encoding/base64" | ||||||
| 	"errors" | 	"errors" | ||||||
|  | 	"net" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"os" | 	"os" | ||||||
| 	"regexp" | 	"regexp" | ||||||
| @@ -202,7 +203,7 @@ func GetLabels(labels map[string]string) (types.Labels, error) { | |||||||
| 	var labelsParsed types.Labels | 	var labelsParsed types.Labels | ||||||
|  |  | ||||||
| 	// Decode the labels into the labels struct | 	// 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 | 	// Check if there was an error | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -368,3 +369,39 @@ func GetBasicAuth(username string, password string) string { | |||||||
| 	// Encode the auth string to base64 | 	// Encode the auth string to base64 | ||||||
| 	return base64.StdEncoding.EncodeToString([]byte(auth)) | 	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