mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2025-10-28 04:35:40 +00:00
feat: header based acls (#337)
* feat: add header decoder * feat: allow for dash substitute over slash for environments like kubernetes * feat: use decoded headers in proxy controller * refactor: simplify decode header to node function * refactor: use stdlib prefix check in header decoder * fix: lowercase key and filter before comparing
This commit is contained in:
@@ -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