diff --git a/go.mod b/go.mod index 495eafa0..8d07b1ca 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 1e267259..98f3f320 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internals/config/structure/structure.go b/internals/config/structure/structure.go index aeed07c0..f8a71921 100644 --- a/internals/config/structure/structure.go +++ b/internals/config/structure/structure.go @@ -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"` diff --git a/internals/proxy/middlewares/auth.go b/internals/proxy/middlewares/auth.go index 5946f27f..91afc979 100644 --- a/internals/proxy/middlewares/auth.go +++ b/internals/proxy/middlewares/auth.go @@ -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). @@ -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 diff --git a/internals/proxy/middlewares/endpoints.go b/internals/proxy/middlewares/endpoints.go index c33e45be..c85f2899 100644 --- a/internals/proxy/middlewares/endpoints.go +++ b/internals/proxy/middlewares/endpoints.go @@ -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 } diff --git a/internals/proxy/middlewares/ipfilter.go b/internals/proxy/middlewares/ipfilter.go index b4138e47..5e118a0f 100644 --- a/internals/proxy/middlewares/ipfilter.go +++ b/internals/proxy/middlewares/ipfilter.go @@ -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) @@ -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 } diff --git a/internals/proxy/middlewares/mapping.go b/internals/proxy/middlewares/mapping.go index 14418ead..be751495 100644 --- a/internals/proxy/middlewares/mapping.go +++ b/internals/proxy/middlewares/mapping.go @@ -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 diff --git a/internals/proxy/middlewares/message.go b/internals/proxy/middlewares/message.go index 51a1748c..d5da10af 100644 --- a/internals/proxy/middlewares/message.go +++ b/internals/proxy/middlewares/message.go @@ -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{} diff --git a/internals/proxy/middlewares/policy.go b/internals/proxy/middlewares/policy.go index 08fbe112..a35d0e2b 100644 --- a/internals/proxy/middlewares/policy.go +++ b/internals/proxy/middlewares/policy.go @@ -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" @@ -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 { @@ -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) @@ -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) { @@ -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: @@ -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, "" } diff --git a/internals/proxy/middlewares/template.go b/internals/proxy/middlewares/template.go index f2eccb91..af7d1a2e 100644 --- a/internals/proxy/middlewares/template.go +++ b/internals/proxy/middlewares/template.go @@ -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{}