diff --git a/README.md b/README.md index 53991ee..69f40c3 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,9 @@ It's designed to be straightforward and efficient, offering a rich set of featur - **Authentication Middleware** - Support for **JWT** with client authorization based on request results. - **Basic-Auth** and **OAuth** authentication mechanisms. +- **Access Policy Middleware** + + The Access Policy middleware controls route access by either `allowing` or `denying` requests based on defined rules. ### Monitoring and Performance - **Logging** diff --git a/docs/index.md b/docs/index.md index 8776ea7..3c65194 100644 --- a/docs/index.md +++ b/docs/index.md @@ -48,6 +48,9 @@ It's designed to be straightforward and efficient, offering a rich set of featur - **Authentication Middleware** - Support for **JWT** with client authorization based on request results. - **Basic-Auth** and **OAuth** authentication mechanisms. +- **Access Policy Middleware** + + The Access Policy middleware controls route access by either `allowing` or `denying` requests based on defined rules. ### Monitoring and Performance - **Logging** diff --git a/docs/middleware/access-policy.md b/docs/middleware/access-policy.md new file mode 100644 index 0000000..1f0df7d --- /dev/null +++ b/docs/middleware/access-policy.md @@ -0,0 +1,33 @@ +--- +title: Access Policy +layout: default +parent: Middleware +nav_order: 3 +--- + + +### Access Policy Middleware +The Access Policy middleware controls route access by either allowing or denying requests based on defined rules. +It supports two actions: `ALLOW` and `DENY`. + +### How It Works +1. **Define an action:** Specify whether the middleware should `ALLOW` or `DENY` access. + +2. **Set sourceRanges:** Provide a list of IP addresses, IP ranges or a CIDR block to which the policy applies. + + Requests originating from these sources will be evaluated according to the specified action. + +#### Example Configuration +Here’s an example of an Access Policy middleware configuration in YAML: + +```yaml +middlewares: + - name: access-policy + type: accessPolicy + rule: + action: DENY # Specify either DENY or ALLOW + sourceRanges: + - 192.168.1.1 # Single IP address + - 10.42.1.1-10.42.1.100 # IP range + - 10.42.1.1/16 # CIDR block +``` diff --git a/docs/middleware/basic.md b/docs/middleware/basic.md index 41c758c..e96dedd 100644 --- a/docs/middleware/basic.md +++ b/docs/middleware/basic.md @@ -2,7 +2,7 @@ title: Basic auth layout: default parent: Middleware -nav_order: 3 +nav_order: 4 --- diff --git a/docs/middleware/jwt.md b/docs/middleware/jwt.md index 7628510..6e25479 100644 --- a/docs/middleware/jwt.md +++ b/docs/middleware/jwt.md @@ -2,7 +2,7 @@ title: JWT Middleware layout: default parent: Middleware -nav_order: 4 +nav_order: 5 --- diff --git a/docs/middleware/oauth.md b/docs/middleware/oauth.md index 13e4068..32f3e34 100644 --- a/docs/middleware/oauth.md +++ b/docs/middleware/oauth.md @@ -2,7 +2,7 @@ title: OAuth auth layout: default parent: Middleware -nav_order: 5 +nav_order: 6 --- # OAuth middleware diff --git a/docs/middleware/rate-limit.md b/docs/middleware/rate-limit.md index 35eec6a..f78c43a 100644 --- a/docs/middleware/rate-limit.md +++ b/docs/middleware/rate-limit.md @@ -1,8 +1,8 @@ --- -title: Rate Limit +title: Rate Limiting layout: default parent: Middleware -nav_order: 6 +nav_order: 7 --- @@ -15,7 +15,7 @@ Example of rate limiting middleware ```yaml middlewares: - name: rate-limit - type: ratelimit #or rateLimit + type: rateLimit #or ratelimit paths: - /* rule: diff --git a/internal/config.go b/internal/config.go index c143022..8b98a43 100644 --- a/internal/config.go +++ b/internal/config.go @@ -307,10 +307,6 @@ func getAccessPoliciesMiddleware(input interface{}) (AccessPolicyRuleMiddleware, if !validateCIDR(ip) { return AccessPolicyRuleMiddleware{}, fmt.Errorf("invalid cidr address") } - if validateCIDR(ip) { - return AccessPolicyRuleMiddleware{}, fmt.Errorf("cidr is not yet supported") - - } } } diff --git a/internal/middlewares/access_policy_middleware.go b/internal/middlewares/access_policy_middleware.go index aaef055..b60d403 100644 --- a/internal/middlewares/access_policy_middleware.go +++ b/internal/middlewares/access_policy_middleware.go @@ -21,6 +21,7 @@ import ( "github.com/jkaninda/goma-gateway/pkg/logger" "net" "net/http" + "strings" ) type AccessPolicy struct { @@ -30,33 +31,103 @@ type AccessPolicy struct { func (access AccessPolicy) AccessPolicyMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - iPs := make(map[string]struct{}) - for _, ip := range access.SourceRanges { - iPs[ip] = struct{}{} - } // Get the client's IP address - ip, _, err := net.SplitHostPort(getRealIP(r)) + clientIP, _, err := net.SplitHostPort(getRealIP(r)) if err != nil { logger.Error("Unable to parse IP address") RespondWithError(w, http.StatusUnauthorized, "Unable to parse IP address") return } - // Check if the IP is in the blocklist - if access.Action == "DENY" { - if _, ok := iPs[ip]; ok { - logger.Error(" %s: IP address in the blocklist, access not allowed", getRealIP(r)) - RespondWithError(w, http.StatusForbidden, http.StatusText(http.StatusForbidden)) + + // Check IP against source ranges + isAllowed := access.Action != "DENY" + for _, entry := range access.SourceRanges { + if isIPAllowed(clientIP, entry) { + if isAllowed { + next.ServeHTTP(w, r) + } else { + logger.Error("%s: IP address in the blocklist, access not allowed", clientIP) + RespondWithError(w, http.StatusForbidden, http.StatusText(http.StatusForbidden)) + } return } } - // Check if the IP is in the allowlist - if _, ok := iPs[ip]; !ok { - logger.Error("%s: IP address not allowed ", getRealIP(r)) - RespondWithError(w, http.StatusForbidden, http.StatusText(http.StatusForbidden)) - return - } - // Continue to the next handler if the authentication is successful - next.ServeHTTP(w, r) - }) + // Final response for disallowed IPs + if isAllowed { + logger.Error("%s: IP address not allowed", clientIP) + RespondWithError(w, http.StatusForbidden, http.StatusText(http.StatusForbidden)) + } else { + next.ServeHTTP(w, r) + } + }) +} + +// isIPAllowed checks if a client IP matches an entry (range, single IP or CIDR block). +func isIPAllowed(clientIP, entry string) bool { + // Handle IP range + if strings.Contains(entry, "-") { + // Handle IP range + startIP, endIP, err := parseIPRange(entry) + return err == nil && ipInRange(clientIP, startIP, endIP) + } + // Handle CIDR + if strings.Contains(entry, "/") { + return ipInCIDR(clientIP, entry) + } + // Handle single IP + return clientIP == entry +} + +// / Parse a range string into start and end IPs +func parseIPRange(rangeStr string) (string, string, error) { + parts := strings.Split(rangeStr, "-") + if len(parts) != 2 { + return "", "", http.ErrAbortHandler + } + + startIP := strings.TrimSpace(parts[0]) + endIP := strings.TrimSpace(parts[1]) + + if net.ParseIP(startIP) == nil || net.ParseIP(endIP) == nil { + return "", "", http.ErrAbortHandler + } + + return startIP, endIP, nil +} + +// Check if an IP is in range +func ipInRange(ipStr, startIP, endIP string) bool { + ip := net.ParseIP(ipStr) + start := net.ParseIP(startIP) + end := net.ParseIP(endIP) + + if ip == nil || start == nil || end == nil { + return false + } + + ipBytes := ip.To4() + startBytes := start.To4() + endBytes := end.To4() + + if ipBytes == nil || startBytes == nil || endBytes == nil { + return false + } + + for i := 0; i < 4; i++ { + if ipBytes[i] < startBytes[i] || ipBytes[i] > endBytes[i] { + return false + } + } + return true +} + +// Check if an IP is within a CIDR block +func ipInCIDR(ipStr, cidr string) bool { + ip := net.ParseIP(ipStr) + _, ipNet, err := net.ParseCIDR(cidr) + if err != nil { + return false + } + return ipNet.Contains(ip) } diff --git a/internal/routes.go b/internal/routes.go index 4047b26..f83b31c 100644 --- a/internal/routes.go +++ b/internal/routes.go @@ -83,11 +83,12 @@ func (gatewayServer GatewayServer) Initialize() *mux.Router { } // Routes health check if !gateway.DisableHealthCheckStatus { - r.HandleFunc("/health/routes", heath.HealthCheckHandler).Methods("GET") + r.HandleFunc("/health/routes", heath.HealthCheckHandler).Methods("GET") // Deprecated + r.HandleFunc("/healthz/routes", heath.HealthCheckHandler).Methods("GET") } // Health check - r.HandleFunc("/health/live", heath.HealthReadyHandler).Methods("GET") + r.HandleFunc("/health/live", heath.HealthReadyHandler).Methods("GET") // Deprecated r.HandleFunc("/readyz", heath.HealthReadyHandler).Methods("GET") r.HandleFunc("/healthz", heath.HealthReadyHandler).Methods("GET") // Enable common exploits @@ -249,14 +250,14 @@ func attachMiddlewares(rIndex int, route Route, gateway Gateway, router *mux.Rou } // AccessPolicy if accessPolicy == mid.Type { - accessPolicy, err := getAccessPoliciesMiddleware(mid.Rule) + a, err := getAccessPoliciesMiddleware(mid.Rule) if err != nil { logger.Error("Error: %v, middleware not applied", err.Error()) } - if len(accessPolicy.SourceRanges) != 0 { + if len(a.SourceRanges) != 0 { access := middlewares.AccessPolicy{ - SourceRanges: accessPolicy.SourceRanges, - Action: accessPolicy.Action, + SourceRanges: a.SourceRanges, + Action: a.Action, } router.Use(access.AccessPolicyMiddleware) }