Skip to content

Commit ec68c90

Browse files
committed
added CEL cyntax validation and graceful shutdown of servers
1 parent 0151f40 commit ec68c90

3 files changed

Lines changed: 122 additions & 56 deletions

File tree

general/cmd/server/main.go

Lines changed: 80 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,56 @@ package main
33
import (
44
"context"
55
"encoding/json"
6-
"log"
7-
"net/http"
8-
"time"
9-
106
"general/internal/db"
117
"general/internal/engine"
128
"general/internal/middleware"
9+
"log"
10+
"net/http"
11+
"os"
12+
"os/signal"
13+
"syscall"
14+
"time"
1315

1416
"github.com/google/uuid"
1517
"github.com/jackc/pgx/v5"
1618
"github.com/jackc/pgx/v5/pgtype"
1719
)
1820

21+
type Config struct {
22+
DBHost string
23+
DBPort string
24+
DBUser string
25+
DBPassword string
26+
DBName string
27+
Port string
28+
JWTSecret string
29+
}
30+
31+
func loadConfig() *Config {
32+
return &Config{
33+
DBHost: getEnv("DB_HOST", "localhost"),
34+
DBPort: getEnv("DB_PORT", "5433"),
35+
DBUser: getEnv("DB_USER", "asguard"),
36+
DBPassword: getEnv("DB_PASSWORD", "devpassword"),
37+
DBName: getEnv("DB_NAME", "general_engine"),
38+
Port: getEnv("PORT", "8083"),
39+
JWTSecret: getEnv("JWT_SECRET", ""),
40+
}
41+
42+
}
43+
44+
func getEnv(key, defaultValue string) string {
45+
if value := os.Getenv(key); value != "" {
46+
return value
47+
}
48+
49+
return defaultValue
50+
}
51+
1952
func main() {
20-
connString := "postgres://asguard:devpassword@localhost:5433/general_engine?sslmode=disable"
2153

54+
cfg := loadConfig()
55+
connString := "postgres://" + cfg.DBUser + ":" + cfg.DBPassword + "@" + cfg.DBHost + ":" + cfg.DBPort + "/" + cfg.DBName + "?sslmode=disable"
2256
conn, err := pgx.Connect(context.Background(), connString)
2357
if err != nil {
2458
log.Fatalf("Failed to connect to database: %v", err)
@@ -68,12 +102,34 @@ func main() {
68102
}
69103
})
70104

71-
// WRAP WITH MIDDLEWARE - This is the key line
72-
handler := middleware.AuthMiddleware(mux)
105+
// WRAP WITH MIDDLEWARE
106+
handler := middleware.AuthMiddleware([]byte(cfg.JWTSecret))(mux)
73107

74-
port := "8083"
75-
log.Printf("General Validation Engine starting on port %s", port)
76-
log.Fatal(http.ListenAndServe(":"+port, handler)) // Use handler, not nil
108+
srv := &http.Server{
109+
Addr: ":" + cfg.Port,
110+
Handler: handler,
111+
}
112+
113+
quit := make(chan os.Signal, 1)
114+
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
115+
116+
go func() {
117+
log.Printf("General Validation Engine starting on port %s", cfg.Port)
118+
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
119+
log.Fatalf("listen: %s\n", err)
120+
}
121+
}()
122+
123+
<-quit
124+
log.Println("Shutting Down Server.....")
125+
126+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
127+
defer cancel()
128+
129+
if err := srv.Shutdown(ctx); err != nil {
130+
log.Fatalf("Server shutdown failed: %v", err)
131+
}
132+
log.Println("Server shutdown successfully")
77133
}
78134

79135
func healthHandler(w http.ResponseWriter, r *http.Request) {
@@ -187,19 +243,25 @@ func createRuleHandler(queries *db.Queries, ruleEngine *engine.RuleEngine) http.
187243
}
188244

189245
// Validate required fields
190-
if req.Name == "" || req.Context == "" || req.Action == "" {
191-
http.Error(w, "Mising required fields", http.StatusBadRequest)
246+
if req.Name == "" || req.Context == "" || req.Condition == "" || req.Action == "" {
247+
http.Error(w, "Missing required fields: name, context, condition, action", http.StatusBadRequest)
192248
return
193249
}
194250

251+
//validate action type
195252
validActions := map[string]bool{"allow": true, "block": true, "challenge": true, "flag": true, "score": true}
196253
if !validActions[req.Action] {
197254
http.Error(w, "Invalid action. Must be: allow, block, challenge, flag, score", http.StatusBadRequest)
198255
return
199256
}
200257

201-
ctx := r.Context()
258+
// validate CEL syntax
259+
if _, err := ruleEngine.Evaluator.CompileRule(req.Condition); err != nil {
260+
http.Error(w, "Invalid CEL syntax: "+err.Error(), http.StatusBadRequest)
261+
return
262+
}
202263

264+
ctx := r.Context()
203265
// Extract tenant ID from JWT context (works with UUID strings and slugs alike)
204266
tenantIDStr := middleware.GetTenantID(ctx)
205267
if tenantIDStr == "" {
@@ -295,6 +357,11 @@ func updateRuleHandler(queries *db.Queries, ruleEngine *engine.RuleEngine, id st
295357
return
296358
}
297359

360+
if _, err := ruleEngine.Evaluator.CompileRule(req.Condition); err != nil {
361+
http.Error(w, "Invalid CEL syntax: "+err.Error(), http.StatusBadRequest)
362+
return
363+
}
364+
298365
ctx := r.Context()
299366

300367
tenantIDStr := middleware.GetTenantID(ctx)

general/internal/engine/engine.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import (
1616
// RuleEngine orchestrates validation
1717
type RuleEngine struct {
1818
queries *db.Queries
19-
evaluator *Evaluator
19+
Evaluator *Evaluator
2020
programeCache sync.Map //// map[string]cel.Program (key = rule ID as string)
2121
}
2222

@@ -29,7 +29,7 @@ func NewRuleEngine(queries *db.Queries) (*RuleEngine, error) {
2929

3030
return &RuleEngine{
3131
queries: queries,
32-
evaluator: eval,
32+
Evaluator: eval,
3333
}, nil
3434
}
3535

@@ -135,7 +135,7 @@ func (re *RuleEngine) Validate(ctx context.Context, tenantID pgtype.UUID, req Va
135135
progIface, ok := re.programeCache.Load(cacheKey)
136136
var program cel.Program
137137
if !ok {
138-
prog, err := re.evaluator.CompileRule(rule.Condition)
138+
prog, err := re.Evaluator.CompileRule(rule.Condition)
139139
if err != nil {
140140
fmt.Printf("Failed to compile rule %s: %v\n", rule.Name, err)
141141
continue
@@ -147,7 +147,7 @@ func (re *RuleEngine) Validate(ctx context.Context, tenantID pgtype.UUID, req Va
147147
}
148148

149149
// Evaluate
150-
matched, err := re.evaluator.Evaluate(program, req.Input)
150+
matched, err := re.Evaluator.Evaluate(program, req.Input)
151151
if err != nil {
152152
fmt.Printf("Failed to evaluate rule %s: %v\n", rule.Name, err)
153153
continue
@@ -294,7 +294,7 @@ func (re *RuleEngine) InvalidateRule(tenantID pgtype.UUID, ruleID pgtype.UUID) {
294294

295295
// StoreRule compiles and caches a rule. Returns error if compilation fails.
296296
func (re *RuleEngine) StoreRule(tenantID pgtype.UUID, ruleID pgtype.UUID, condition string) error {
297-
prog, err := re.evaluator.CompileRule(condition)
297+
prog, err := re.Evaluator.CompileRule(condition)
298298
if err != nil {
299299
return err
300300
}

general/internal/middleware/autho.go

Lines changed: 37 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"context"
55
"fmt"
66
"net/http"
7-
"os"
87
"strings"
98

109
"general/internal/auth"
@@ -14,51 +13,51 @@ import (
1413

1514
type tenantKey struct{}
1615

17-
var jwtSecret = []byte(os.Getenv("JWT_SECRET"))
16+
func AuthMiddleware(secret []byte) func(http.Handler) http.Handler {
17+
return func(next http.Handler) http.Handler {
18+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
19+
if r.URL.Path == "/health" {
20+
next.ServeHTTP(w, r)
21+
return
22+
}
1823

19-
func AuthMiddleware(next http.Handler) http.Handler {
20-
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
21-
if r.URL.Path == "/health" {
22-
next.ServeHTTP(w, r)
23-
return
24-
}
24+
authHeader := r.Header.Get("Authorization")
25+
if authHeader == "" {
26+
http.Error(w, `{"error":"missing authorization header"}`, http.StatusUnauthorized)
27+
return
28+
}
2529

26-
authHeader := r.Header.Get("Authorization")
27-
if authHeader == "" {
28-
http.Error(w, `{"error":"missing authorization header"}`, http.StatusUnauthorized)
29-
return
30-
}
30+
parts := strings.SplitN(authHeader, " ", 2)
31+
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
32+
http.Error(w, `{"error":"invalid authorization header format"}`, http.StatusUnauthorized)
33+
return
34+
}
3135

32-
parts := strings.SplitN(authHeader, " ", 2)
33-
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
34-
http.Error(w, `{"error":"invalid authorization header format"}`, http.StatusUnauthorized)
35-
return
36-
}
36+
tokenString := parts[1]
3737

38-
tokenString := parts[1]
38+
// Use the shared struct
39+
var claims auth.Claims
40+
token, err := jwt.ParseWithClaims(tokenString, &claims, func(token *jwt.Token) (interface{}, error) {
41+
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
42+
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
43+
}
44+
return secret, nil
45+
})
3946

40-
// Use the shared struct
41-
var claims auth.Claims
42-
token, err := jwt.ParseWithClaims(tokenString, &claims, func(token *jwt.Token) (interface{}, error) {
43-
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
44-
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
47+
if err != nil || !token.Valid {
48+
http.Error(w, `{"error":"invalid token"}`, http.StatusUnauthorized)
49+
return
4550
}
46-
return jwtSecret, nil
47-
})
4851

49-
if err != nil || !token.Valid {
50-
http.Error(w, `{"error":"invalid token"}`, http.StatusUnauthorized)
51-
return
52-
}
53-
54-
if claims.TenantID == "" {
55-
http.Error(w, `{"error":"missing tenant_id"}`, http.StatusUnauthorized)
56-
return
57-
}
52+
if claims.TenantID == "" {
53+
http.Error(w, `{"error":"missing tenant_id"}`, http.StatusUnauthorized)
54+
return
55+
}
5856

59-
ctx := context.WithValue(r.Context(), tenantKey{}, claims.TenantID)
60-
next.ServeHTTP(w, r.WithContext(ctx))
61-
})
57+
ctx := context.WithValue(r.Context(), tenantKey{}, claims.TenantID)
58+
next.ServeHTTP(w, r.WithContext(ctx))
59+
})
60+
}
6261
}
6362

6463
func GetTenantID(ctx context.Context) string {

0 commit comments

Comments
 (0)