-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgraphql_validation_ratelimit.go
More file actions
125 lines (106 loc) · 3.17 KB
/
Copy pathgraphql_validation_ratelimit.go
File metadata and controls
125 lines (106 loc) · 3.17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
package graph
// RateLimitRule implements per-user rate limiting based on query complexity
type RateLimitRule struct {
BaseRule
costPerUnit int
getBudget func(userID string) (int, error)
bypassRoles []string
}
// RateLimitOption configures rate limiting behavior
type RateLimitOption func(*RateLimitRule)
// WithCostPerUnit sets the cost multiplier per complexity unit (default: 1)
func WithCostPerUnit(cost int) RateLimitOption {
return func(r *RateLimitRule) {
r.costPerUnit = cost
}
}
// WithBudgetFunc sets the function to get user's remaining budget
func WithBudgetFunc(fn func(userID string) (int, error)) RateLimitOption {
return func(r *RateLimitRule) {
r.getBudget = fn
}
}
// WithBypassRoles sets roles that bypass rate limiting (e.g., "admin", "service")
func WithBypassRoles(roles ...string) RateLimitOption {
return func(r *RateLimitRule) {
r.bypassRoles = roles
}
}
// NewRateLimitRule creates a new rate limiting rule with optional configuration
//
// Example:
// NewRateLimitRule(
// WithBudgetFunc(getBudgetFromRedis),
// WithCostPerUnit(2),
// WithBypassRoles("admin", "service"),
// )
func NewRateLimitRule(opts ...RateLimitOption) ValidationRule {
rule := &RateLimitRule{
BaseRule: NewBaseRule("RateLimitRule"),
costPerUnit: 1,
bypassRoles: []string{},
}
for _, opt := range opts {
opt(rule)
}
return rule
}
// HasIDInterface - implement this on your user struct for rate limiting
type HasIDInterface interface {
GetID() string
}
func (r *RateLimitRule) Validate(ctx *ValidationContext) error {
// Skip if no budget function configured
if r.getBudget == nil {
return nil
}
// Skip if user not authenticated
if ctx.UserDetails == nil {
return nil
}
// Get user ID - try to type assert to HasIDInterface
userWithID, ok := ctx.UserDetails.(HasIDInterface)
if !ok {
return r.NewError("rate limiting requires user to implement GetID() method")
}
// Check if user has bypass role
if len(r.bypassRoles) > 0 {
if userWithRoles, ok := ctx.UserDetails.(HasRolesInterface); ok {
for _, bypassRole := range r.bypassRoles {
if userWithRoles.HasRole(bypassRole) {
return nil
}
}
}
}
// Get user's budget
budget, err := r.getBudget(userWithID.GetID())
if err != nil {
return r.NewErrorf("failed to check rate limit: %v", err)
}
// Calculate query cost
complexity := calculateQueryComplexity(ctx.Document, 1)
cost := complexity * r.costPerUnit
// Check if cost exceeds budget
if cost > budget {
return r.NewErrorf("query cost %d exceeds available budget %d", cost, budget)
}
return nil
}
// SimpleBudgetFunc creates a simple budget function that returns a fixed budget
// Useful for testing or simple rate limiting scenarios
func SimpleBudgetFunc(budget int) func(string) (int, error) {
return func(userID string) (int, error) {
return budget, nil
}
}
// PerUserBudgetFunc creates a budget function with per-user budgets
// Useful for different tier users or testing
func PerUserBudgetFunc(budgets map[string]int, defaultBudget int) func(string) (int, error) {
return func(userID string) (int, error) {
if budget, ok := budgets[userID]; ok {
return budget, nil
}
return defaultBudget, nil
}
}