mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-10-30 05:35:44 +00:00 
			
		
		
		
	Compare commits
	
		
			6 Commits
		
	
	
		
			cbe31d442d
			...
			feat/heade
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 1a83e1c811 | ||
|   | b15f91967b | ||
|   | 6e88a89730 | ||
|   | 78920cba64 | ||
|   | b6bc3d0020 | ||
|   | ca772ed24f | 
| @@ -123,53 +123,53 @@ type RedirectQuery struct { | |||||||
| 	RedirectURI string `url:"redirect_uri"` | 	RedirectURI string `url:"redirect_uri"` | ||||||
| } | } | ||||||
|  |  | ||||||
| // Labels | // App config | ||||||
|  |  | ||||||
| type Labels struct { | type AppConfigs struct { | ||||||
| 	Apps map[string]AppLabels | 	Apps map[string]App | ||||||
| } | } | ||||||
|  |  | ||||||
| type AppLabels struct { | type App struct { | ||||||
| 	Config   ConfigLabels | 	Config   AppConfig | ||||||
| 	Users    UsersLabels | 	Users    AppUsers | ||||||
| 	OAuth    OAuthLabels | 	OAuth    AppOAuth | ||||||
| 	IP       IPLabels | 	IP       AppIP | ||||||
| 	Response ResponseLabels | 	Response AppResponse | ||||||
| 	Path     PathLabels | 	Path     AppPath | ||||||
| } | } | ||||||
|  |  | ||||||
| type ConfigLabels struct { | type AppConfig struct { | ||||||
| 	Domain string | 	Domain string | ||||||
| } | } | ||||||
|  |  | ||||||
| type UsersLabels struct { | type AppUsers struct { | ||||||
| 	Allow string | 	Allow string | ||||||
| 	Block string | 	Block string | ||||||
| } | } | ||||||
|  |  | ||||||
| type OAuthLabels struct { | type AppOAuth struct { | ||||||
| 	Whitelist string | 	Whitelist string | ||||||
| 	Groups    string | 	Groups    string | ||||||
| } | } | ||||||
|  |  | ||||||
| type IPLabels struct { | type AppIP struct { | ||||||
| 	Allow  []string | 	Allow  []string | ||||||
| 	Block  []string | 	Block  []string | ||||||
| 	Bypass []string | 	Bypass []string | ||||||
| } | } | ||||||
|  |  | ||||||
| type ResponseLabels struct { | type AppResponse struct { | ||||||
| 	Headers   []string | 	Headers   []string | ||||||
| 	BasicAuth BasicAuthLabels | 	BasicAuth AppBasicAuth | ||||||
| } | } | ||||||
|  |  | ||||||
| type BasicAuthLabels struct { | type AppBasicAuth struct { | ||||||
| 	Username     string | 	Username     string | ||||||
| 	Password     string | 	Password     string | ||||||
| 	PasswordFile string | 	PasswordFile string | ||||||
| } | } | ||||||
|  |  | ||||||
| type PathLabels struct { | type AppPath struct { | ||||||
| 	Allow string | 	Allow string | ||||||
| 	Block string | 	Block string | ||||||
| } | } | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ import ( | |||||||
| 	"tinyauth/internal/config" | 	"tinyauth/internal/config" | ||||||
| 	"tinyauth/internal/service" | 	"tinyauth/internal/service" | ||||||
| 	"tinyauth/internal/utils" | 	"tinyauth/internal/utils" | ||||||
|  | 	"tinyauth/internal/utils/decoders" | ||||||
|  |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/google/go-querystring/query" | 	"github.com/google/go-querystring/query" | ||||||
| @@ -67,6 +68,16 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { | |||||||
| 	proto := c.Request.Header.Get("X-Forwarded-Proto") | 	proto := c.Request.Header.Get("X-Forwarded-Proto") | ||||||
| 	host := c.Request.Header.Get("X-Forwarded-Host") | 	host := c.Request.Header.Get("X-Forwarded-Host") | ||||||
|  |  | ||||||
|  | 	var app config.App | ||||||
|  |  | ||||||
|  | 	headers, err := decoders.DecodeHeaders(utils.NormalizeHeaders(c.Request.Header)) | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error().Err(err).Msg("Failed to decode headers") | ||||||
|  | 		controller.handleError(c, req, isBrowser) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	labels, err := controller.docker.GetLabels(host) | 	labels, err := controller.docker.GetLabels(host) | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -75,10 +86,21 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if len(headers.Apps) > 0 { | ||||||
|  | 		for k, v := range headers.Apps { | ||||||
|  | 			log.Debug().Str("app", k).Msg("Using headers for app config instead of labels") | ||||||
|  | 			app = v | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		log.Debug().Msg("No app config found in headers, using labels") | ||||||
|  | 		app = labels | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	clientIP := c.ClientIP() | 	clientIP := c.ClientIP() | ||||||
|  |  | ||||||
| 	if controller.auth.IsBypassedIP(labels.IP, clientIP) { | 	if controller.auth.IsBypassedIP(app.IP, clientIP) { | ||||||
| 		controller.setHeaders(c, labels) | 		controller.setHeaders(c, app) | ||||||
| 		c.JSON(200, gin.H{ | 		c.JSON(200, gin.H{ | ||||||
| 			"status":  200, | 			"status":  200, | ||||||
| 			"message": "Authenticated", | 			"message": "Authenticated", | ||||||
| @@ -86,7 +108,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	authEnabled, err := controller.auth.IsAuthEnabled(uri, labels.Path) | 	authEnabled, err := controller.auth.IsAuthEnabled(uri, app.Path) | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Error().Err(err).Msg("Failed to check if auth is enabled for resource") | 		log.Error().Err(err).Msg("Failed to check if auth is enabled for resource") | ||||||
| @@ -96,7 +118,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { | |||||||
|  |  | ||||||
| 	if !authEnabled { | 	if !authEnabled { | ||||||
| 		log.Debug().Msg("Authentication disabled for resource, allowing access") | 		log.Debug().Msg("Authentication disabled for resource, allowing access") | ||||||
| 		controller.setHeaders(c, labels) | 		controller.setHeaders(c, app) | ||||||
| 		c.JSON(200, gin.H{ | 		c.JSON(200, gin.H{ | ||||||
| 			"status":  200, | 			"status":  200, | ||||||
| 			"message": "Authenticated", | 			"message": "Authenticated", | ||||||
| @@ -104,7 +126,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if !controller.auth.CheckIP(labels.IP, clientIP) { | 	if !controller.auth.CheckIP(app.IP, clientIP) { | ||||||
| 		if req.Proxy == "nginx" || !isBrowser { | 		if req.Proxy == "nginx" || !isBrowser { | ||||||
| 			c.JSON(401, gin.H{ | 			c.JSON(401, gin.H{ | ||||||
| 				"status":  401, | 				"status":  401, | ||||||
| @@ -147,7 +169,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if userContext.IsLoggedIn { | 	if userContext.IsLoggedIn { | ||||||
| 		appAllowed := controller.auth.IsResourceAllowed(c, userContext, labels) | 		appAllowed := controller.auth.IsResourceAllowed(c, userContext, app) | ||||||
|  |  | ||||||
| 		if !appAllowed { | 		if !appAllowed { | ||||||
| 			log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User not allowed to access resource") | 			log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User not allowed to access resource") | ||||||
| @@ -181,7 +203,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if userContext.OAuth { | 		if userContext.OAuth { | ||||||
| 			groupOK := controller.auth.IsInOAuthGroup(c, userContext, labels.OAuth.Groups) | 			groupOK := controller.auth.IsInOAuthGroup(c, userContext, app.OAuth.Groups) | ||||||
|  |  | ||||||
| 			if !groupOK { | 			if !groupOK { | ||||||
| 				log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User OAuth groups do not match resource requirements") | 				log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User OAuth groups do not match resource requirements") | ||||||
| @@ -221,7 +243,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { | |||||||
| 		c.Header("Remote-Email", utils.SanitizeHeader(userContext.Email)) | 		c.Header("Remote-Email", utils.SanitizeHeader(userContext.Email)) | ||||||
| 		c.Header("Remote-Groups", utils.SanitizeHeader(userContext.OAuthGroups)) | 		c.Header("Remote-Groups", utils.SanitizeHeader(userContext.OAuthGroups)) | ||||||
|  |  | ||||||
| 		controller.setHeaders(c, labels) | 		controller.setHeaders(c, app) | ||||||
|  |  | ||||||
| 		c.JSON(200, gin.H{ | 		c.JSON(200, gin.H{ | ||||||
| 			"status":  200, | 			"status":  200, | ||||||
| @@ -251,7 +273,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { | |||||||
| 	c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/login?%s", controller.config.AppURL, queries.Encode())) | 	c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/login?%s", controller.config.AppURL, queries.Encode())) | ||||||
| } | } | ||||||
|  |  | ||||||
| func (controller *ProxyController) setHeaders(c *gin.Context, labels config.AppLabels) { | func (controller *ProxyController) setHeaders(c *gin.Context, labels config.App) { | ||||||
| 	c.Header("Authorization", c.Request.Header.Get("Authorization")) | 	c.Header("Authorization", c.Request.Header.Get("Authorization")) | ||||||
|  |  | ||||||
| 	headers := utils.ParseHeaders(labels.Response.Headers) | 	headers := utils.ParseHeaders(labels.Response.Headers) | ||||||
|   | |||||||
| @@ -285,7 +285,7 @@ func (auth *AuthService) UserAuthConfigured() bool { | |||||||
| 	return len(auth.config.Users) > 0 || auth.ldap != nil | 	return len(auth.config.Users) > 0 || auth.ldap != nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (auth *AuthService) IsResourceAllowed(c *gin.Context, context config.UserContext, labels config.AppLabels) bool { | func (auth *AuthService) IsResourceAllowed(c *gin.Context, context config.UserContext, labels config.App) bool { | ||||||
| 	if context.OAuth { | 	if context.OAuth { | ||||||
| 		log.Debug().Msg("Checking OAuth whitelist") | 		log.Debug().Msg("Checking OAuth whitelist") | ||||||
| 		return utils.CheckFilter(labels.OAuth.Whitelist, context.Email) | 		return utils.CheckFilter(labels.OAuth.Whitelist, context.Email) | ||||||
| @@ -322,7 +322,7 @@ func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context config.UserConte | |||||||
| 	return false | 	return false | ||||||
| } | } | ||||||
|  |  | ||||||
| func (auth *AuthService) IsAuthEnabled(uri string, path config.PathLabels) (bool, error) { | func (auth *AuthService) IsAuthEnabled(uri string, path config.AppPath) (bool, error) { | ||||||
| 	// Check for block list | 	// Check for block list | ||||||
| 	if path.Block != "" { | 	if path.Block != "" { | ||||||
| 		regex, err := regexp.Compile(path.Block) | 		regex, err := regexp.Compile(path.Block) | ||||||
| @@ -364,7 +364,7 @@ func (auth *AuthService) GetBasicAuth(c *gin.Context) *config.User { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func (auth *AuthService) CheckIP(labels config.IPLabels, ip string) bool { | func (auth *AuthService) CheckIP(labels config.AppIP, ip string) bool { | ||||||
| 	for _, blocked := range labels.Block { | 	for _, blocked := range labels.Block { | ||||||
| 		res, err := utils.FilterIP(blocked, ip) | 		res, err := utils.FilterIP(blocked, ip) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| @@ -398,7 +398,7 @@ func (auth *AuthService) CheckIP(labels config.IPLabels, ip string) bool { | |||||||
| 	return true | 	return true | ||||||
| } | } | ||||||
|  |  | ||||||
| func (auth *AuthService) IsBypassedIP(labels config.IPLabels, ip string) bool { | func (auth *AuthService) IsBypassedIP(labels config.AppIP, ip string) bool { | ||||||
| 	for _, bypassed := range labels.Bypass { | 	for _, bypassed := range labels.Bypass { | ||||||
| 		res, err := utils.FilterIP(bypassed, ip) | 		res, err := utils.FilterIP(bypassed, ip) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ import ( | |||||||
| 	"context" | 	"context" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"tinyauth/internal/config" | 	"tinyauth/internal/config" | ||||||
| 	"tinyauth/internal/utils" | 	"tinyauth/internal/utils/decoders" | ||||||
|  |  | ||||||
| 	container "github.com/docker/docker/api/types/container" | 	container "github.com/docker/docker/api/types/container" | ||||||
| 	"github.com/docker/docker/client" | 	"github.com/docker/docker/client" | ||||||
| @@ -55,17 +55,17 @@ func (docker *DockerService) DockerConnected() bool { | |||||||
| 	return err == nil | 	return err == nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (docker *DockerService) GetLabels(appDomain string) (config.AppLabels, error) { | func (docker *DockerService) GetLabels(appDomain string) (config.App, error) { | ||||||
| 	isConnected := docker.DockerConnected() | 	isConnected := docker.DockerConnected() | ||||||
|  |  | ||||||
| 	if !isConnected { | 	if !isConnected { | ||||||
| 		log.Debug().Msg("Docker not connected, returning empty labels") | 		log.Debug().Msg("Docker not connected, returning empty labels") | ||||||
| 		return config.AppLabels{}, nil | 		return config.App{}, nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	containers, err := docker.GetContainers() | 	containers, err := docker.GetContainers() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return config.AppLabels{}, err | 		return config.App{}, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for _, ctr := range containers { | 	for _, ctr := range containers { | ||||||
| @@ -75,7 +75,7 @@ func (docker *DockerService) GetLabels(appDomain string) (config.AppLabels, erro | |||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		labels, err := utils.GetLabels(inspect.Config.Labels) | 		labels, err := decoders.DecodeLabels(inspect.Config.Labels) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Warn().Str("id", ctr.ID).Err(err).Msg("Error getting container labels, skipping") | 			log.Warn().Str("id", ctr.ID).Err(err).Msg("Error getting container labels, skipping") | ||||||
| 			continue | 			continue | ||||||
| @@ -95,5 +95,5 @@ func (docker *DockerService) GetLabels(appDomain string) (config.AppLabels, erro | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	log.Debug().Msg("No matching container found, returning empty labels") | 	log.Debug().Msg("No matching container found, returning empty labels") | ||||||
| 	return config.AppLabels{}, nil | 	return config.App{}, nil | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										119
									
								
								internal/utils/decoders/header_decoder.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								internal/utils/decoders/header_decoder.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,119 @@ | |||||||
|  | package decoders | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"sort" | ||||||
|  | 	"strings" | ||||||
|  | 	"tinyauth/internal/config" | ||||||
|  |  | ||||||
|  | 	"github.com/traefik/paerser/parser" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // Based on: https://github.com/traefik/paerser/blob/master/parser/labels_decode.go (Apache 2.0 License) | ||||||
|  |  | ||||||
|  | func DecodeHeaders(headers map[string]string) (config.AppConfigs, error) { | ||||||
|  | 	var app config.AppConfigs | ||||||
|  |  | ||||||
|  | 	err := decodeHeadersHelper(headers, &app, "tinyauth", "tinyauth-apps") | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		return config.AppConfigs{}, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return app, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func decodeHeadersHelper(headers map[string]string, element any, rootName string, filters ...string) error { | ||||||
|  | 	node, err := decodeHeadersToNode(headers, rootName, filters...) | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	opts := parser.MetadataOpts{TagName: "header", AllowSliceAsStruct: true} | ||||||
|  | 	err = parser.AddMetadata(element, node, opts) | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return parser.Fill(element, node, parser.FillerOpts{AllowSliceAsStruct: true}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func decodeHeadersToNode(headers map[string]string, rootName string, filters ...string) (*parser.Node, error) { | ||||||
|  | 	sortedKeys := sortKeys(headers, filters) | ||||||
|  |  | ||||||
|  | 	var node *parser.Node | ||||||
|  |  | ||||||
|  | 	for i, key := range sortedKeys { | ||||||
|  | 		split := strings.Split(strings.ToLower(key), "-") | ||||||
|  |  | ||||||
|  | 		if split[0] != rootName { | ||||||
|  | 			return nil, fmt.Errorf("invalid header root %s", split[0]) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		for _, v := range split { | ||||||
|  | 			if v == "" { | ||||||
|  | 				return nil, fmt.Errorf("invalid element: %s", key) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if i == 0 { | ||||||
|  | 			node = &parser.Node{} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		decodeHeaderToNode(node, split, headers[key]) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return node, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func decodeHeaderToNode(root *parser.Node, path []string, value string) { | ||||||
|  | 	if len(root.Name) == 0 { | ||||||
|  | 		root.Name = path[0] | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if len(path) > 1 { | ||||||
|  | 		node := containsNode(root.Children, path[1]) | ||||||
|  |  | ||||||
|  | 		if node != nil { | ||||||
|  | 			decodeHeaderToNode(node, path[1:], value) | ||||||
|  | 		} else { | ||||||
|  | 			child := &parser.Node{Name: path[1]} | ||||||
|  | 			decodeHeaderToNode(child, path[1:], value) | ||||||
|  | 			root.Children = append(root.Children, child) | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		root.Value = value | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func containsNode(nodes []*parser.Node, name string) *parser.Node { | ||||||
|  | 	for _, node := range nodes { | ||||||
|  | 		if strings.EqualFold(node.Name, name) { | ||||||
|  | 			return node | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func sortKeys(headers map[string]string, filters []string) []string { | ||||||
|  | 	var sortedKeys []string | ||||||
|  |  | ||||||
|  | 	for key := range headers { | ||||||
|  | 		if len(filters) == 0 { | ||||||
|  | 			sortedKeys = append(sortedKeys, key) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		for _, filter := range filters { | ||||||
|  | 			if strings.HasPrefix(strings.ToLower(key), strings.ToLower(filter)) { | ||||||
|  | 				sortedKeys = append(sortedKeys, key) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	sort.Strings(sortedKeys) | ||||||
|  | 	return sortedKeys | ||||||
|  | } | ||||||
							
								
								
									
										73
									
								
								internal/utils/decoders/header_decoder_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								internal/utils/decoders/header_decoder_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | |||||||
|  | package decoders_test | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"reflect" | ||||||
|  | 	"testing" | ||||||
|  | 	"tinyauth/internal/config" | ||||||
|  | 	"tinyauth/internal/utils/decoders" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestDecodeHeaders(t *testing.T) { | ||||||
|  | 	// Variables | ||||||
|  | 	expected := config.AppConfigs{ | ||||||
|  | 		Apps: map[string]config.App{ | ||||||
|  | 			"foo": { | ||||||
|  | 				Config: config.AppConfig{ | ||||||
|  | 					Domain: "example.com", | ||||||
|  | 				}, | ||||||
|  | 				Users: config.AppUsers{ | ||||||
|  | 					Allow: "user1,user2", | ||||||
|  | 					Block: "user3", | ||||||
|  | 				}, | ||||||
|  | 				OAuth: config.AppOAuth{ | ||||||
|  | 					Whitelist: "somebody@example.com", | ||||||
|  | 					Groups:    "group3", | ||||||
|  | 				}, | ||||||
|  | 				IP: config.AppIP{ | ||||||
|  | 					Allow:  []string{"10.71.0.1/24", "10.71.0.2"}, | ||||||
|  | 					Block:  []string{"10.10.10.10", "10.0.0.0/24"}, | ||||||
|  | 					Bypass: []string{"192.168.1.1"}, | ||||||
|  | 				}, | ||||||
|  | 				Response: config.AppResponse{ | ||||||
|  | 					Headers: []string{"X-Foo=Bar", "X-Baz=Qux"}, | ||||||
|  | 					BasicAuth: config.AppBasicAuth{ | ||||||
|  | 						Username:     "admin", | ||||||
|  | 						Password:     "password", | ||||||
|  | 						PasswordFile: "/path/to/passwordfile", | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				Path: config.AppPath{ | ||||||
|  | 					Allow: "/public", | ||||||
|  | 					Block: "/private", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	test := map[string]string{ | ||||||
|  | 		"Tinyauth-Apps-Foo-Config-Domain":                   "example.com", | ||||||
|  | 		"Tinyauth-Apps-Foo-Users-Allow":                     "user1,user2", | ||||||
|  | 		"Tinyauth-Apps-Foo-Users-Block":                     "user3", | ||||||
|  | 		"Tinyauth-Apps-Foo-OAuth-Whitelist":                 "somebody@example.com", | ||||||
|  | 		"Tinyauth-Apps-Foo-OAuth-Groups":                    "group3", | ||||||
|  | 		"Tinyauth-Apps-Foo-IP-Allow":                        "10.71.0.1/24,10.71.0.2", | ||||||
|  | 		"Tinyauth-Apps-Foo-IP-Block":                        "10.10.10.10,10.0.0.0/24", | ||||||
|  | 		"Tinyauth-Apps-Foo-IP-Bypass":                       "192.168.1.1", | ||||||
|  | 		"Tinyauth-Apps-Foo-Response-Headers":                "X-Foo=Bar,X-Baz=Qux", | ||||||
|  | 		"Tinyauth-Apps-Foo-Response-BasicAuth-Username":     "admin", | ||||||
|  | 		"Tinyauth-Apps-Foo-Response-BasicAuth-Password":     "password", | ||||||
|  | 		"Tinyauth-Apps-Foo-Response-BasicAuth-PasswordFile": "/path/to/passwordfile", | ||||||
|  | 		"Tinyauth-Apps-Foo-Path-Allow":                      "/public", | ||||||
|  | 		"Tinyauth-Apps-Foo-Path-Block":                      "/private", | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Test | ||||||
|  | 	result, err := decoders.DecodeHeaders(test) | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Unexpected error: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if !reflect.DeepEqual(expected, result) { | ||||||
|  | 		t.Fatalf("Expected %v but got %v", expected, result) | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										19
									
								
								internal/utils/decoders/label_decoder.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								internal/utils/decoders/label_decoder.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | package decoders | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"tinyauth/internal/config" | ||||||
|  |  | ||||||
|  | 	"github.com/traefik/paerser/parser" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func DecodeLabels(labels map[string]string) (config.AppConfigs, error) { | ||||||
|  | 	var appLabels config.AppConfigs | ||||||
|  |  | ||||||
|  | 	err := parser.Decode(labels, &appLabels, "tinyauth", "tinyauth.apps") | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		return config.AppConfigs{}, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return appLabels, nil | ||||||
|  | } | ||||||
							
								
								
									
										73
									
								
								internal/utils/decoders/label_decoder_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								internal/utils/decoders/label_decoder_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | |||||||
|  | package decoders_test | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"reflect" | ||||||
|  | 	"testing" | ||||||
|  | 	"tinyauth/internal/config" | ||||||
|  | 	"tinyauth/internal/utils/decoders" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestDecodeLabels(t *testing.T) { | ||||||
|  | 	// Variables | ||||||
|  | 	expected := config.AppConfigs{ | ||||||
|  | 		Apps: map[string]config.App{ | ||||||
|  | 			"foo": { | ||||||
|  | 				Config: config.AppConfig{ | ||||||
|  | 					Domain: "example.com", | ||||||
|  | 				}, | ||||||
|  | 				Users: config.AppUsers{ | ||||||
|  | 					Allow: "user1,user2", | ||||||
|  | 					Block: "user3", | ||||||
|  | 				}, | ||||||
|  | 				OAuth: config.AppOAuth{ | ||||||
|  | 					Whitelist: "somebody@example.com", | ||||||
|  | 					Groups:    "group3", | ||||||
|  | 				}, | ||||||
|  | 				IP: config.AppIP{ | ||||||
|  | 					Allow:  []string{"10.71.0.1/24", "10.71.0.2"}, | ||||||
|  | 					Block:  []string{"10.10.10.10", "10.0.0.0/24"}, | ||||||
|  | 					Bypass: []string{"192.168.1.1"}, | ||||||
|  | 				}, | ||||||
|  | 				Response: config.AppResponse{ | ||||||
|  | 					Headers: []string{"X-Foo=Bar", "X-Baz=Qux"}, | ||||||
|  | 					BasicAuth: config.AppBasicAuth{ | ||||||
|  | 						Username:     "admin", | ||||||
|  | 						Password:     "password", | ||||||
|  | 						PasswordFile: "/path/to/passwordfile", | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				Path: config.AppPath{ | ||||||
|  | 					Allow: "/public", | ||||||
|  | 					Block: "/private", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	test := map[string]string{ | ||||||
|  | 		"tinyauth.apps.foo.config.domain":                   "example.com", | ||||||
|  | 		"tinyauth.apps.foo.users.allow":                     "user1,user2", | ||||||
|  | 		"tinyauth.apps.foo.users.block":                     "user3", | ||||||
|  | 		"tinyauth.apps.foo.oauth.whitelist":                 "somebody@example.com", | ||||||
|  | 		"tinyauth.apps.foo.oauth.groups":                    "group3", | ||||||
|  | 		"tinyauth.apps.foo.ip.allow":                        "10.71.0.1/24,10.71.0.2", | ||||||
|  | 		"tinyauth.apps.foo.ip.block":                        "10.10.10.10,10.0.0.0/24", | ||||||
|  | 		"tinyauth.apps.foo.ip.bypass":                       "192.168.1.1", | ||||||
|  | 		"tinyauth.apps.foo.response.headers":                "X-Foo=Bar,X-Baz=Qux", | ||||||
|  | 		"tinyauth.apps.foo.response.basicauth.username":     "admin", | ||||||
|  | 		"tinyauth.apps.foo.response.basicauth.password":     "password", | ||||||
|  | 		"tinyauth.apps.foo.response.basicauth.passwordfile": "/path/to/passwordfile", | ||||||
|  | 		"tinyauth.apps.foo.path.allow":                      "/public", | ||||||
|  | 		"tinyauth.apps.foo.path.block":                      "/private", | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Test | ||||||
|  | 	result, err := decoders.DecodeLabels(test) | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Unexpected error: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if reflect.DeepEqual(expected, result) == false { | ||||||
|  | 		t.Fatalf("Expected %v but got %v", expected, result) | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -3,22 +3,8 @@ package utils | |||||||
| import ( | import ( | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"tinyauth/internal/config" |  | ||||||
| 
 |  | ||||||
| 	"github.com/traefik/paerser/parser" |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func GetLabels(labels map[string]string) (config.Labels, error) { |  | ||||||
| 	var labelsParsed config.Labels |  | ||||||
| 
 |  | ||||||
| 	err := parser.Decode(labels, &labelsParsed, "tinyauth", "tinyauth.apps") |  | ||||||
| 	if err != nil { |  | ||||||
| 		return config.Labels{}, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return labelsParsed, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func ParseHeaders(headers []string) map[string]string { | func ParseHeaders(headers []string) map[string]string { | ||||||
| 	headerMap := make(map[string]string) | 	headerMap := make(map[string]string) | ||||||
| 	for _, header := range headers { | 	for _, header := range headers { | ||||||
| @@ -46,3 +32,13 @@ func SanitizeHeader(header string) string { | |||||||
| 		return -1 | 		return -1 | ||||||
| 	}, header) | 	}, header) | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func NormalizeHeaders(headers http.Header) map[string]string { | ||||||
|  | 	var result = make(map[string]string) | ||||||
|  | 
 | ||||||
|  | 	for key, values := range headers { | ||||||
|  | 		result[key] = strings.Join(values, ",") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return result | ||||||
|  | } | ||||||
| @@ -46,6 +46,8 @@ func GetBasicAuth(username string, password string) string { | |||||||
| } | } | ||||||
|  |  | ||||||
| func FilterIP(filter string, ip string) (bool, error) { | func FilterIP(filter string, ip string) (bool, error) { | ||||||
|  | 	filter = strings.Replace(filter, "-", "/", -1) | ||||||
|  |  | ||||||
| 	ipAddr := net.ParseIP(ip) | 	ipAddr := net.ParseIP(ip) | ||||||
|  |  | ||||||
| 	if strings.Contains(filter, "/") { | 	if strings.Contains(filter, "/") { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user