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:
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 (
|
||||
"net/http"
|
||||
"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 {
|
||||
headerMap := make(map[string]string)
|
||||
for _, header := range headers {
|
||||
@@ -46,3 +32,13 @@ func SanitizeHeader(header string) string {
|
||||
return -1
|
||||
}, 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) {
|
||||
filter = strings.Replace(filter, "-", "/", -1)
|
||||
|
||||
ipAddr := net.ParseIP(ip)
|
||||
|
||||
if strings.Contains(filter, "/") {
|
||||
|
||||
Reference in New Issue
Block a user