chore: sync submodule state (parent ref update)
Made-with: Cursor
This commit is contained in:
25
gateway/go/Dockerfile
Normal file
25
gateway/go/Dockerfile
Normal file
@@ -0,0 +1,25 @@
|
||||
FROM golang:1.21-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy go mod files
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy source
|
||||
COPY . .
|
||||
|
||||
# Build
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk --no-cache add ca-certificates curl
|
||||
|
||||
WORKDIR /root/
|
||||
|
||||
COPY --from=builder /app/main .
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ["./main"]
|
||||
68
gateway/go/cache/cache.go
vendored
Normal file
68
gateway/go/cache/cache.go
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
)
|
||||
|
||||
type Cache struct {
|
||||
client *redis.Client
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func New(redisURL string) (*Cache, error) {
|
||||
opt, err := redis.ParseURL(redisURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := redis.NewClient(opt)
|
||||
ctx := context.Background()
|
||||
|
||||
// Test connection
|
||||
if err := client.Ping(ctx).Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Cache{
|
||||
client: client,
|
||||
ctx: ctx,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Cache) Get(key string) ([]byte, error) {
|
||||
val, err := c.client.Get(c.ctx, key).Result()
|
||||
if err == redis.Nil {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte(val), nil
|
||||
}
|
||||
|
||||
func (c *Cache) Set(key string, value []byte, ttl time.Duration) error {
|
||||
return c.client.Set(c.ctx, key, value, ttl).Err()
|
||||
}
|
||||
|
||||
func (c *Cache) Delete(key string) error {
|
||||
return c.client.Del(c.ctx, key).Err()
|
||||
}
|
||||
|
||||
func (c *Cache) InvalidatePattern(pattern string) error {
|
||||
keys, err := c.client.Keys(c.ctx, pattern).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(keys) > 0 {
|
||||
return c.client.Del(c.ctx, keys...).Err()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cache) Close() error {
|
||||
return c.client.Close()
|
||||
}
|
||||
39
gateway/go/config/config.go
Normal file
39
gateway/go/config/config.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Port string
|
||||
BackendURL string
|
||||
PolicyEngineURL string
|
||||
RedisURL string
|
||||
CacheTTL int
|
||||
JWTSecret string
|
||||
LogLevel string
|
||||
}
|
||||
|
||||
func Load() *Config {
|
||||
return &Config{
|
||||
Port: getEnv("GATEWAY_PORT", "8080"),
|
||||
BackendURL: getEnv("BACKEND_URL", "http://localhost:3000"),
|
||||
PolicyEngineURL: getEnv("POLICY_ENGINE_URL", "http://localhost:3000"),
|
||||
RedisURL: getEnv("REDIS_URL", "redis://localhost:6379"),
|
||||
CacheTTL: getEnvInt("CACHE_TTL", 120),
|
||||
JWTSecret: getEnv("JWT_SECRET", ""),
|
||||
LogLevel: getEnv("LOG_LEVEL", "info"),
|
||||
}
|
||||
}
|
||||
|
||||
func getEnv(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func getEnvInt(key string, defaultValue int) int {
|
||||
// Simplified - in production, use strconv.Atoi
|
||||
return defaultValue
|
||||
}
|
||||
9
gateway/go/go.mod
Normal file
9
gateway/go/go.mod
Normal file
@@ -0,0 +1,9 @@
|
||||
module solacenet-gateway
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/go-redis/redis/v8 v8.11.5
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
)
|
||||
1
gateway/go/go.sum
Normal file
1
gateway/go/go.sum
Normal file
@@ -0,0 +1 @@
|
||||
# Placeholder - run `go mod tidy` to generate actual checksums
|
||||
13
gateway/go/handlers/health.go
Normal file
13
gateway/go/handlers/health.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// HealthHandler handles health check requests
|
||||
func HealthHandler(c *gin.Context) {
|
||||
c.JSON(200, gin.H{
|
||||
"status": "healthy",
|
||||
"service": "solacenet-gateway",
|
||||
})
|
||||
}
|
||||
58
gateway/go/handlers/proxy.go
Normal file
58
gateway/go/handlers/proxy.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"solacenet-gateway/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ProxyHandler proxies requests to backend services
|
||||
func ProxyHandler(cfg *config.Config) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Build backend URL
|
||||
backendURL := cfg.BackendURL + c.Request.URL.Path
|
||||
if c.Request.URL.RawQuery != "" {
|
||||
backendURL += "?" + c.Request.URL.RawQuery
|
||||
}
|
||||
|
||||
// Create request
|
||||
req, err := http.NewRequest(c.Request.Method, backendURL, c.Request.Body)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to create request",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Copy headers
|
||||
for key, values := range c.Request.Header {
|
||||
for _, value := range values {
|
||||
req.Header.Add(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// Make request
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{
|
||||
"error": "Failed to reach backend",
|
||||
})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Copy response headers
|
||||
for key, values := range resp.Header {
|
||||
for _, value := range values {
|
||||
c.Header(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// Copy response body
|
||||
c.Status(resp.StatusCode)
|
||||
io.Copy(c.Writer, resp.Body)
|
||||
}
|
||||
}
|
||||
53
gateway/go/main.go
Normal file
53
gateway/go/main.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"solacenet-gateway/cache"
|
||||
"solacenet-gateway/config"
|
||||
"solacenet-gateway/handlers"
|
||||
"solacenet-gateway/middleware"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := config.Load()
|
||||
|
||||
// Initialize Redis cache
|
||||
redisCache, err := cache.New(cfg.RedisURL)
|
||||
if err != nil {
|
||||
log.Printf("Warning: Redis not available, caching disabled: %v", err)
|
||||
redisCache = nil
|
||||
}
|
||||
defer func() {
|
||||
if redisCache != nil {
|
||||
redisCache.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
// Set up Gin router
|
||||
if cfg.LogLevel == "production" {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
router := gin.Default()
|
||||
|
||||
// Middleware
|
||||
router.Use(middleware.AuthMiddleware(cfg))
|
||||
if redisCache != nil {
|
||||
router.Use(middleware.CapabilityCheckMiddleware(cfg, redisCache))
|
||||
}
|
||||
router.Use(middleware.RateLimitMiddleware())
|
||||
|
||||
// Health check
|
||||
router.GET("/health", handlers.HealthHandler)
|
||||
|
||||
// Proxy handler for backend services
|
||||
router.Any("/api/*path", handlers.ProxyHandler(cfg))
|
||||
|
||||
// Start server
|
||||
log.Printf("SolaceNet Gateway starting on port %s", cfg.Port)
|
||||
if err := router.Run(":" + cfg.Port); err != nil {
|
||||
log.Fatal("Failed to start server:", err)
|
||||
}
|
||||
}
|
||||
57
gateway/go/middleware/auth.go
Normal file
57
gateway/go/middleware/auth.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"solacenet-gateway/config"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
// AuthMiddleware validates JWT tokens
|
||||
func AuthMiddleware(cfg *config.Config) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "Authorization header required",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Extract token
|
||||
parts := strings.Split(authHeader, " ")
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "Invalid authorization header format",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
tokenString := parts[1]
|
||||
|
||||
// Parse and validate token
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(cfg.JWTSecret), nil
|
||||
})
|
||||
|
||||
if err != nil || !token.Valid {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "Invalid token",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Extract claims
|
||||
if claims, ok := token.Claims.(jwt.MapClaims); ok {
|
||||
c.Set("userID", claims["sub"])
|
||||
c.Set("tenantID", claims["tenantId"])
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
152
gateway/go/middleware/capability-check.go
Normal file
152
gateway/go/middleware/capability-check.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"solacenet-gateway/cache"
|
||||
"solacenet-gateway/config"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type PolicyDecisionRequest struct {
|
||||
TenantID string `json:"tenantId"`
|
||||
ProgramID string `json:"programId,omitempty"`
|
||||
CapabilityID string `json:"capabilityId"`
|
||||
Region string `json:"region,omitempty"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
Actor string `json:"actor,omitempty"`
|
||||
Context map[string]interface{} `json:"context,omitempty"`
|
||||
}
|
||||
|
||||
type PolicyDecisionResponse struct {
|
||||
Allowed bool `json:"allowed"`
|
||||
Mode string `json:"mode"`
|
||||
Limits map[string]interface{} `json:"limits,omitempty"`
|
||||
ReasonCode string `json:"reasonCode,omitempty"`
|
||||
DecisionID string `json:"decisionId"`
|
||||
}
|
||||
|
||||
// CapabilityCheckMiddleware checks if a capability is enabled before routing
|
||||
func CapabilityCheckMiddleware(cfg *config.Config, cache *cache.Cache) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Extract capability ID from request path or header
|
||||
capabilityID := c.GetHeader("X-Capability-ID")
|
||||
if capabilityID == "" {
|
||||
// Try to extract from path pattern
|
||||
// This is a simplified version - adjust based on your routing
|
||||
capabilityID = extractCapabilityFromPath(c.Request.URL.Path)
|
||||
}
|
||||
|
||||
if capabilityID == "" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// Extract context from request
|
||||
tenantID := c.GetHeader("X-Tenant-ID")
|
||||
programID := c.GetHeader("X-Program-ID")
|
||||
region := c.GetHeader("X-Region")
|
||||
channel := c.GetHeader("X-Channel")
|
||||
actor := c.GetHeader("X-Actor")
|
||||
|
||||
// Check cache first
|
||||
cacheKey := fmt.Sprintf("policy:decision:%s:%s:%s:%s:%s:%s",
|
||||
tenantID, programID, capabilityID, region, channel, actor)
|
||||
|
||||
if cached, err := cache.Get(cacheKey); err == nil && cached != nil {
|
||||
var decision PolicyDecisionResponse
|
||||
if json.Unmarshal(cached, &decision) == nil {
|
||||
if !decision.Allowed {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Capability not available",
|
||||
"reasonCode": decision.ReasonCode,
|
||||
"mode": decision.Mode,
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Set("policyDecision", decision)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Call policy engine
|
||||
decisionReq := PolicyDecisionRequest{
|
||||
TenantID: tenantID,
|
||||
ProgramID: programID,
|
||||
CapabilityID: capabilityID,
|
||||
Region: region,
|
||||
Channel: channel,
|
||||
Actor: actor,
|
||||
}
|
||||
|
||||
decision, err := callPolicyEngine(cfg, decisionReq)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to check capability",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Cache the decision
|
||||
if decisionJSON, err := json.Marshal(decision); err == nil {
|
||||
cache.Set(cacheKey, decisionJSON, time.Duration(cfg.CacheTTL)*time.Second)
|
||||
}
|
||||
|
||||
if !decision.Allowed {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Capability not available",
|
||||
"reasonCode": decision.ReasonCode,
|
||||
"mode": decision.Mode,
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("policyDecision", decision)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func callPolicyEngine(cfg *config.Config, req PolicyDecisionRequest) (*PolicyDecisionResponse, error) {
|
||||
reqBody, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := http.Post(
|
||||
fmt.Sprintf("%s/api/v1/solacenet/policy/decide", cfg.PolicyEngineURL),
|
||||
"application/json",
|
||||
bytes.NewBuffer(reqBody),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var decision PolicyDecisionResponse
|
||||
if err := json.Unmarshal(body, &decision); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &decision, nil
|
||||
}
|
||||
|
||||
func extractCapabilityFromPath(path string) string {
|
||||
// Simplified extraction - adjust based on your routing patterns
|
||||
// Example: /api/v1/payments/... -> "payment-gateway"
|
||||
// This should be configured based on your actual routing
|
||||
return ""
|
||||
}
|
||||
14
gateway/go/middleware/rate-limit.go
Normal file
14
gateway/go/middleware/rate-limit.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// RateLimitMiddleware implements rate limiting
|
||||
// In production, use a proper rate limiting library like golang.org/x/time/rate
|
||||
func RateLimitMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Simplified rate limiting - implement proper rate limiting in production
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user