Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
62b975a
compile regex => if error use == matching
CodeShellDev Jan 7, 2026
dcd372d
explicit block > allow
CodeShellDev Jan 7, 2026
3e7da87
return after Bad Body
CodeShellDev Jan 7, 2026
b0650c2
debug body templating
CodeShellDev Jan 7, 2026
f848eba
debug body templating
CodeShellDev Jan 7, 2026
c386e70
more debugging
CodeShellDev Jan 7, 2026
e9e7230
bump gotl for bugfix
CodeShellDev Jan 7, 2026
a0e5b20
Merge branch 'v1.5.0' into feat/regex-in-field-policies
CodeShellDev Jan 7, 2026
e543844
scope policies under respective field
CodeShellDev Jan 7, 2026
1c23cc8
fix index out of range error in getPolicies
CodeShellDev Jan 7, 2026
441ec78
debug
CodeShellDev Jan 7, 2026
2e302d5
wait for true, dont return early in doPoliciesApply()
CodeShellDev Jan 7, 2026
4a60d8e
typo fix
CodeShellDev Jan 7, 2026
e35e9ba
debug regex matching for field policies
CodeShellDev Jan 7, 2026
d834cb0
refactor blocking behaviours
CodeShellDev Jan 7, 2026
04057b6
.
CodeShellDev Jan 8, 2026
8e5d048
revert
CodeShellDev Jan 8, 2026
9595504
get tokens on each request to enable quick reload
CodeShellDev Jan 8, 2026
3b39757
use `IP_FILTER` instead of `ENDPOINTS` 🤦
CodeShellDev Jan 8, 2026
7496f36
use float64 for json
CodeShellDev Jan 8, 2026
6aac76c
convert into float64 for json
CodeShellDev Jan 8, 2026
a3db452
debug floats
CodeShellDev Jan 8, 2026
1613736
more debugging
CodeShellDev Jan 8, 2026
f6b9b6b
check type
CodeShellDev Jan 8, 2026
3133ad0
debug value type
CodeShellDev Jan 8, 2026
5891448
bump gotl version for float support in stringutils
CodeShellDev Jan 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module github.com/codeshelldev/secured-signal-api

go 1.25.5

require github.com/codeshelldev/gotl v0.0.10
require github.com/codeshelldev/gotl v0.0.13

require go.uber.org/zap v1.27.1

Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
github.com/codeshelldev/gotl v0.0.10 h1:/2HOPXTlG1HplbXvkB1cZG6NQlGHCiZfWR1pMj6X55M=
github.com/codeshelldev/gotl v0.0.10/go.mod h1:rDkJma6eQSRfCr7ieX9/esn3/uAWNzjHfpjlr9j6FFk=
github.com/codeshelldev/gotl v0.0.12 h1:VM3W6hiEfPgK+cCLT70S004tYAdhQWXD72FtFqCF+2Q=
github.com/codeshelldev/gotl v0.0.12/go.mod h1:rDkJma6eQSRfCr7ieX9/esn3/uAWNzjHfpjlr9j6FFk=
github.com/codeshelldev/gotl v0.0.13 h1:3sittUEi9D02nP06FSSxAduHJggFlfdMrEz3iXaySmU=
github.com/codeshelldev/gotl v0.0.13/go.mod h1:rDkJma6eQSRfCr7ieX9/esn3/uAWNzjHfpjlr9j6FFk=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
Expand Down
2 changes: 1 addition & 1 deletion internals/config/structure/structure.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ type FieldMapping struct {

type ACCESS struct {
ENDPOINTS []string `koanf:"endpoints"`
FIELD_POLICIES map[string]FieldPolicy `koanf:"fieldpolicies" childtransform:"default"`
FIELD_POLICIES map[string][]FieldPolicy `koanf:"fieldpolicies" childtransform:"default"`
RATE_LIMITING RateLimiting `koanf:"ratelimiting"`
IP_FILTER []string `koanf:"ipfilter"`
TRUSTED_IPS []string `koanf:"trustedips"`
Expand Down
14 changes: 7 additions & 7 deletions internals/proxy/middlewares/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,6 @@ const tokenKey contextKey = "token"
const isAuthKey contextKey = "isAuthenticated"

func authHandler(next http.Handler) http.Handler {
tokenKeys := maps.Keys(config.ENV.CONFIGS)
tokens := slices.Collect(tokenKeys)

if tokens == nil {
tokens = []string{}
}

var authChain = NewAuthChain().
Use(BearerAuth).
Use(BasicAuth).
Expand All @@ -38,6 +31,13 @@ func authHandler(next http.Handler) http.Handler {
Use(PathAuth)

return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
tokenKeys := maps.Keys(config.ENV.CONFIGS)
tokens := slices.Collect(tokenKeys)

if tokens == nil {
tokens = []string{}
}

if config.ENV.INSECURE || len(tokens) <= 0 {
next.ServeHTTP(w, req)
return
Expand Down
14 changes: 7 additions & 7 deletions internals/proxy/middlewares/endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,16 +82,16 @@ func isEndpointBlocked(endpoint string, endpoints []string) bool {
return true
}

// only allowed endpoints -> block anything not allowed
if len(allowed) > 0 && len(blocked) == 0 {
// allow rules -> default deny
if len(allowed) > 0 {
return true
}

// only blocked endpoints -> allow anything not blocked
if len(blocked) > 0 && len(allowed) == 0 {
// only block rules -> default allow
if len(blocked) > 0 {
return false
}

// default: allow all
return false
// safety net -> block
return true
}
10 changes: 5 additions & 5 deletions internals/proxy/middlewares/ipfilter.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func ipFilterHandler(next http.Handler) http.Handler {
ipFilter := conf.SETTINGS.ACCESS.IP_FILTER

if ipFilter == nil {
ipFilter = getConfig("").SETTINGS.ACCESS.ENDPOINTS
ipFilter = getConfig("").SETTINGS.ACCESS.IP_FILTER
}

ip := getContext[net.IP](req, clientIPKey)
Expand Down Expand Up @@ -80,16 +80,16 @@ func isIPBlocked(ip net.IP, ipfilter []string) (bool) {
return true
}

// if any allow rules exist, default is deny
// allow rules -> default deny
if len(allowed) > 0 {
return true
}

// only blocked ips -> allow anything not blocked
// only block rules -> default allow
if len(blocked) > 0 {
return false
}

// default: allow all
return false
// safety net -> block
return true
}
1 change: 1 addition & 0 deletions internals/proxy/middlewares/mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func mappingHandler(next http.Handler) http.Handler {
if err != nil {
logger.Error("Could not get Request Body: ", err.Error())
http.Error(w, "Bad Request: invalid body", http.StatusBadRequest)
return
}

var modifiedBody bool
Expand Down
1 change: 1 addition & 0 deletions internals/proxy/middlewares/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ func messageHandler(next http.Handler) http.Handler {
if err != nil {
logger.Error("Could not get Request Body: ", err.Error())
http.Error(w, "Bad Request: invalid body", http.StatusBadRequest)
return
}

bodyData := map[string]any{}
Expand Down
120 changes: 77 additions & 43 deletions internals/proxy/middlewares/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"net/http"
"reflect"
"regexp"

request "github.com/codeshelldev/gotl/pkg/request"
"github.com/codeshelldev/secured-signal-api/internals/config/structure"
Expand Down Expand Up @@ -32,6 +33,7 @@ func policyHandler(next http.Handler) http.Handler {
if err != nil {
logger.Error("Could not get Request Body: ", err.Error())
http.Error(w, "Bad Request: invalid body", http.StatusBadRequest)
return
}

if body.Empty {
Expand All @@ -40,7 +42,7 @@ func policyHandler(next http.Handler) http.Handler {

headerData := request.GetReqHeaders(req)

shouldBlock, field := doBlock(body.Data, headerData, policies)
shouldBlock, field := isBlockedByPolicy(body.Data, headerData, policies)

if shouldBlock {
logger.Warn("Client tried to use blocked field: ", field)
Expand All @@ -52,20 +54,20 @@ func policyHandler(next http.Handler) http.Handler {
})
}

func getPolicies(policies map[string]structure.FieldPolicy) (map[string]structure.FieldPolicy, map[string]structure.FieldPolicy) {
blockedFields := map[string]structure.FieldPolicy{}
allowedFields := map[string]structure.FieldPolicy{}
func getPolicies(policies []structure.FieldPolicy) ([]structure.FieldPolicy, []structure.FieldPolicy) {
blocked := []structure.FieldPolicy{}
allowed := []structure.FieldPolicy{}

for field, policy := range policies {
for _, policy := range policies {
switch policy.Action {
case "block":
blockedFields[field] = policy
blocked = append(blocked, policy)
case "allow":
allowedFields[field] = policy
allowed = append(allowed, policy)
}
}

return allowedFields, blockedFields
return allowed, blocked
}

func getField(key string, body map[string]any, headers map[string][]string) (any, error) {
Expand All @@ -77,34 +79,53 @@ func getField(key string, body map[string]any, headers map[string][]string) (any
return value, nil
}

return value, errors.New("field not found")
return nil, errors.New("field not found")
}

func doPoliciesApply(body map[string]any, headers map[string][]string, policies map[string]structure.FieldPolicy) (bool, string) {
for key, policy := range policies {
value, err := getField(key, body, headers)
func doPoliciesApply(key string, body map[string]any, headers map[string][]string, policies []structure.FieldPolicy) (bool, string) {
value, err := getField(key, body, headers)

if err != nil {
continue
}
if err != nil {
return false, ""
}

for _, policy := range policies {
switch asserted := value.(type) {
case string:
policyValue, ok := policy.Value.(string)

re, err := regexp.Compile(policyValue)

if err == nil {
if re.MatchString(asserted) {
return true, key
}
continue
}

if ok && asserted == policyValue {
return true, key
}
case int:
policyValue, ok := policy.Value.(int);
policyValue, ok := policy.Value.(int)

if ok && asserted == policyValue {
return true, key
}
case bool:
policyValue, ok := policy.Value.(bool)
case float64:
var policyValue float64

// needed for json
switch assertedValue := policy.Value.(type) {
case int:
policyValue = float64(assertedValue)
case float64:
policyValue = assertedValue
default:
continue
}

if ok && asserted == policyValue {
if asserted == policyValue {
return true, key
}
default:
Expand All @@ -117,38 +138,51 @@ func doPoliciesApply(body map[string]any, headers map[string][]string, policies
return false, ""
}

func doBlock(body map[string]any, headers map[string][]string, policies map[string]structure.FieldPolicy) (bool, string) {
if len(policies) == 0 {
func isBlockedByPolicy(body map[string]any, headers map[string][]string, policies map[string][]structure.FieldPolicy) (bool, string) {
if len(policies) == 0 || policies == nil {
// default: allow all
return false, ""
}

allowed, blocked := getPolicies(policies)
for field, policy := range policies {
if len(policy) == 0 || policy == nil {
continue
}

value, _ := getField(field, body, headers)

var cause string
if value == nil {
continue
}

isExplicitlyAllowed, cause := doPoliciesApply(body, headers, allowed)
isExplicitlyBlocked, cause := doPoliciesApply(body, headers, blocked)

// explicit allow > block
if isExplicitlyAllowed {
return false, cause
}

if isExplicitlyBlocked {
return true, cause
}
allowed, blocked := getPolicies(policy)

// only allow policies -> block anything not allowed
if len(allowed) > 0 && len(blocked) == 0 {
return true, cause
}
isExplicitlyAllowed, cause := doPoliciesApply(field, body, headers, allowed)
isExplicitlyBlocked, cause := doPoliciesApply(field, body, headers, blocked)

// only block polcicies -> allow anything not blocked
if len(blocked) > 0 && len(allowed) == 0 {
return false, cause
// explicit allow > block
if isExplicitlyAllowed {
return false, cause
}

if isExplicitlyBlocked {
return true, cause
}

// allow rules -> default deny
if len(allowed) > 0 {
return true, cause
}

// only block rules -> default allow
if len(blocked) > 0 {
return false, cause
}

// safety net -> block
return true, "safety net"
}

// no match -> default: block all
return true, cause
// default: allow all
return false, ""
}
1 change: 1 addition & 0 deletions internals/proxy/middlewares/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func templateHandler(next http.Handler) http.Handler {
if err != nil {
logger.Error("Could not get Request Body: ", err.Error())
http.Error(w, "Bad Request: invalid body", http.StatusBadRequest)
return
}

bodyData := map[string]any{}
Expand Down