gitlab-labkit 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/labkit/fips.rb +2 -2
- data/lib/labkit/rate_limit/evaluator.rb +37 -16
- data/lib/labkit/rate_limit/limiter.rb +13 -4
- data/lib/labkit/rate_limit.rb +5 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e99765f42b600bc7e6307b75665409c3e3ee3b6aabf69f5bdb14d65fe20d8cdc
|
|
4
|
+
data.tar.gz: b2623750accd4178e7d938ebf87abbb6e1d40f14d4e252b9654f015d4141ce3c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cfc0d196a34588965f17b6598b7bf8b1cdaa8d90d79e62bf5a4d75e07537473ee4a8011c7718e18af64d4ec216a85b7bd3a3099523404f6bd7c892b5a7b4ae35
|
|
7
|
+
data.tar.gz: fdb892b50ca446d76d999e7397c57df1539f6724e31ea698b440a7d08a5544e25246d2a4d7ed5bb4516dc5592b31fc7bb1860d29baac43bdc3b94052d011c24d
|
data/lib/labkit/fips.rb
CHANGED
|
@@ -25,7 +25,7 @@ module Labkit
|
|
|
25
25
|
return true if %w[1 true yes].include?(ENV["FIPS_MODE"])
|
|
26
26
|
|
|
27
27
|
# Otherwise, attempt to auto-detect FIPS mode from OpenSSL
|
|
28
|
-
return true if OpenSSL.fips_mode
|
|
28
|
+
return true if ::OpenSSL.fips_mode
|
|
29
29
|
|
|
30
30
|
false
|
|
31
31
|
end
|
|
@@ -44,7 +44,7 @@ module Labkit
|
|
|
44
44
|
|
|
45
45
|
def use_openssl_digest(ruby_algorithm, openssl_algorithm)
|
|
46
46
|
::Digest.send(:remove_const, ruby_algorithm) # rubocop:disable GitlabSecurity/PublicSend
|
|
47
|
-
::Digest.const_set(ruby_algorithm, OpenSSL::Digest.const_get(openssl_algorithm, false))
|
|
47
|
+
::Digest.const_set(ruby_algorithm, ::OpenSSL::Digest.const_get(openssl_algorithm, false))
|
|
48
48
|
end
|
|
49
49
|
end
|
|
50
50
|
end
|
|
@@ -65,8 +65,8 @@ module Labkit
|
|
|
65
65
|
@logger = logger
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
-
def check(identifier, cost: 1)
|
|
69
|
-
check_rules(identifier, cost)
|
|
68
|
+
def check(identifier, cost: 1, rule_context: nil)
|
|
69
|
+
check_rules(identifier, cost, rule_context)
|
|
70
70
|
rescue StandardError => e
|
|
71
71
|
# Intentionally broad: fail-open applies to any unexpected error (network,
|
|
72
72
|
# timeout, OOM) not only Redis protocol errors.
|
|
@@ -78,8 +78,8 @@ module Labkit
|
|
|
78
78
|
# Read-without-increment counterpart to {#check}. Same matching and Result
|
|
79
79
|
# shape; the underlying Redis counter is not mutated and the TTL is not
|
|
80
80
|
# extended. A missing Redis key is treated as count=0 (matched, not exceeded).
|
|
81
|
-
def peek(identifier)
|
|
82
|
-
peek_rules(identifier)
|
|
81
|
+
def peek(identifier, rule_context: nil)
|
|
82
|
+
peek_rules(identifier, rule_context)
|
|
83
83
|
rescue StandardError => e
|
|
84
84
|
report_error_metrics
|
|
85
85
|
log_error(e, identifier)
|
|
@@ -95,7 +95,7 @@ module Labkit
|
|
|
95
95
|
# is missing the count_distinct key fail open + log + bump errors_total, and
|
|
96
96
|
# the loop continues to the next rule (the rule is treated as not applicable
|
|
97
97
|
# rather than aborting the whole evaluation).
|
|
98
|
-
def check_rules(identifier, cost)
|
|
98
|
+
def check_rules(identifier, cost, rule_context)
|
|
99
99
|
@rules.each do |rule|
|
|
100
100
|
next unless rule_matches?(rule, identifier)
|
|
101
101
|
|
|
@@ -105,7 +105,7 @@ module Labkit
|
|
|
105
105
|
next
|
|
106
106
|
end
|
|
107
107
|
|
|
108
|
-
result = evaluate_rule(rule, identifier, cost)
|
|
108
|
+
result = evaluate_rule(rule, identifier, cost, rule_context)
|
|
109
109
|
report_matched_metrics(result)
|
|
110
110
|
return result unless rule.action == :log
|
|
111
111
|
end
|
|
@@ -120,12 +120,12 @@ module Labkit
|
|
|
120
120
|
# peek does not need the count_distinct identifier key - it reads SCARD on
|
|
121
121
|
# the rule-keyed compound key, which contains the cardinality across all
|
|
122
122
|
# members. So missing-key fail-open does not apply here.
|
|
123
|
-
def peek_rules(identifier)
|
|
123
|
+
def peek_rules(identifier, rule_context)
|
|
124
124
|
@rules.each do |rule|
|
|
125
125
|
next if rule.action == :log
|
|
126
126
|
next unless rule_matches?(rule, identifier)
|
|
127
127
|
|
|
128
|
-
return peek_rule(rule, identifier)
|
|
128
|
+
return peek_rule(rule, identifier, rule_context)
|
|
129
129
|
end
|
|
130
130
|
|
|
131
131
|
Result.new(matched: false, action: :allow)
|
|
@@ -140,10 +140,10 @@ module Labkit
|
|
|
140
140
|
value.nil? || value.to_s.empty?
|
|
141
141
|
end
|
|
142
142
|
|
|
143
|
-
def evaluate_rule(rule, identifier, cost)
|
|
143
|
+
def evaluate_rule(rule, identifier, cost, rule_context)
|
|
144
144
|
redis_key = build_redis_key(rule, identifier)
|
|
145
|
-
resolved_limit = Integer(resolve_value(rule.limit))
|
|
146
|
-
resolved_period = Integer(resolve_value(rule.period))
|
|
145
|
+
resolved_limit = Integer(resolve_value(rule.limit, rule_context))
|
|
146
|
+
resolved_period = Integer(resolve_value(rule.period, rule_context))
|
|
147
147
|
|
|
148
148
|
# cost is ignored for count_distinct rules: SADD is binary (a member is
|
|
149
149
|
# either added or not), and the post-add count is SCARD regardless.
|
|
@@ -157,10 +157,10 @@ module Labkit
|
|
|
157
157
|
build_result(rule, resolved_limit, resolved_period, count, ttl)
|
|
158
158
|
end
|
|
159
159
|
|
|
160
|
-
def peek_rule(rule, identifier)
|
|
160
|
+
def peek_rule(rule, identifier, rule_context)
|
|
161
161
|
redis_key = build_redis_key(rule, identifier)
|
|
162
|
-
resolved_limit = Integer(resolve_value(rule.limit))
|
|
163
|
-
resolved_period = Integer(resolve_value(rule.period))
|
|
162
|
+
resolved_limit = Integer(resolve_value(rule.limit, rule_context))
|
|
163
|
+
resolved_period = Integer(resolve_value(rule.period, rule_context))
|
|
164
164
|
|
|
165
165
|
count, ttl = rule.count_distinct ? scard_with_ttl(redis_key) : read_with_ttl(redis_key)
|
|
166
166
|
build_result(rule, resolved_limit, resolved_period, count, ttl)
|
|
@@ -195,8 +195,29 @@ module Labkit
|
|
|
195
195
|
value.to_s
|
|
196
196
|
end
|
|
197
197
|
|
|
198
|
-
|
|
199
|
-
|
|
198
|
+
# Resolve a limit/period value. Plain values pass through; callables are
|
|
199
|
+
# invoked according to their arity:
|
|
200
|
+
#
|
|
201
|
+
# - Zero-arity callables call with no args (e.g. -> { ApplicationSetting.current.foo }).
|
|
202
|
+
# - Callables with arity >= 1 receive +rule_context+, which may be nil
|
|
203
|
+
# if the caller didn't pass it. They must therefore handle nil -
|
|
204
|
+
# typically with `ctx&.[](:key) || default`.
|
|
205
|
+
# - Variadic callables (negative arity, e.g. ->(*args) { ... }) take
|
|
206
|
+
# the zero-arg path. Opt into rule_context by writing the lambda
|
|
207
|
+
# with exactly one required parameter: ->(ctx) { ... }. This avoids
|
|
208
|
+
# the footgun where ->(*args) silently receives [rule_context] and
|
|
209
|
+
# the caller's overrides never take effect.
|
|
210
|
+
# - Callables that respond to +call+ but not +arity+ (e.g. a class with
|
|
211
|
+
# `def call` and no explicit arity) take the zero-arg path, preserving
|
|
212
|
+
# the pre-rule_context behaviour for custom callable objects.
|
|
213
|
+
#
|
|
214
|
+
# This lets rules carry callables that depend on per-request context (e.g.
|
|
215
|
+
# per-namespace settings) without rebuilding the Rule on every call or
|
|
216
|
+
# smuggling state through globals.
|
|
217
|
+
def resolve_value(val, rule_context = nil)
|
|
218
|
+
return val unless val.respond_to?(:call)
|
|
219
|
+
|
|
220
|
+
val.respond_to?(:arity) && val.arity >= 1 ? val.call(rule_context) : val.call
|
|
200
221
|
end
|
|
201
222
|
|
|
202
223
|
def encode_char_value(value)
|
|
@@ -35,10 +35,18 @@ module Labkit
|
|
|
35
35
|
# @param cost [Numeric] amount to add to the counter. Defaults to 1
|
|
36
36
|
# (count-mode). Pass a non-1 Numeric for cost-mode counters such as
|
|
37
37
|
# resource-usage limits; passing 0 reads the counter without writing.
|
|
38
|
+
# @param rule_context [Hash, nil] optional per-request context passed to
|
|
39
|
+
# one-arity callables on +limit+/+period+. Lets rules resolve dynamic
|
|
40
|
+
# configuration (e.g. per-namespace settings) without rebuilding the
|
|
41
|
+
# Rule or doing out-of-band DB queries. Zero-arity callables ignore it.
|
|
42
|
+
# The key contract is owned by the rule's callable, not validated
|
|
43
|
+
# here: if the rule reads ctx[:limit] and the caller passes
|
|
44
|
+
# ctx[:lmit], the callable's fallback branch fires silently. Keep
|
|
45
|
+
# the rule definition and the call site colocated.
|
|
38
46
|
# @return [Result]
|
|
39
|
-
def check(identifier, cost: 1)
|
|
47
|
+
def check(identifier, cost: 1, rule_context: nil)
|
|
40
48
|
id = identifier.is_a?(Identifier) ? identifier : Identifier.new(identifier)
|
|
41
|
-
@evaluator.check(id, cost: cost)
|
|
49
|
+
@evaluator.check(id, cost: cost, rule_context: rule_context)
|
|
42
50
|
end
|
|
43
51
|
|
|
44
52
|
# Read the current rate-limit state without incrementing the counter.
|
|
@@ -54,10 +62,11 @@ module Labkit
|
|
|
54
62
|
# open identically to {#check}.
|
|
55
63
|
#
|
|
56
64
|
# @param identifier [Identifier, Hash] caller attributes for this request
|
|
65
|
+
# @param rule_context [Hash, nil] see {#check}
|
|
57
66
|
# @return [Result]
|
|
58
|
-
def peek(identifier)
|
|
67
|
+
def peek(identifier, rule_context: nil)
|
|
59
68
|
id = identifier.is_a?(Identifier) ? identifier : Identifier.new(identifier)
|
|
60
|
-
@evaluator.peek(id)
|
|
69
|
+
@evaluator.peek(id, rule_context: rule_context)
|
|
61
70
|
end
|
|
62
71
|
|
|
63
72
|
private
|
data/lib/labkit/rate_limit.rb
CHANGED
|
@@ -44,9 +44,12 @@ module Labkit
|
|
|
44
44
|
# @param redis [Object, nil] Redis client; falls back to config.redis
|
|
45
45
|
# @param logger [Logger, nil] logger; falls back to config.logger
|
|
46
46
|
# @param cost [Numeric] amount to add to the counter; see Limiter#check
|
|
47
|
+
# @param rule_context [Hash, nil] per-request context for one-arity
|
|
48
|
+
# callables on rule limit/period; see Limiter#check
|
|
47
49
|
# @return [Result]
|
|
48
|
-
def check(name:, identifier:, rules:, redis: nil, logger: nil, cost: 1)
|
|
49
|
-
Limiter.new(name: name, rules: rules, redis: redis, logger: logger)
|
|
50
|
+
def check(name:, identifier:, rules:, redis: nil, logger: nil, cost: 1, rule_context: nil)
|
|
51
|
+
Limiter.new(name: name, rules: rules, redis: redis, logger: logger)
|
|
52
|
+
.check(identifier, cost: cost, rule_context: rule_context)
|
|
50
53
|
end
|
|
51
54
|
end
|
|
52
55
|
end
|