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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1457da9fa7cec587b47834870a4eb60f5a21c9966111d1d9edd0e7e0a49b4ff3
4
- data.tar.gz: 631e56481d2be50e976a16aefa6fd6a250759089b56dde0414af059fd55c4ba7
3
+ metadata.gz: e99765f42b600bc7e6307b75665409c3e3ee3b6aabf69f5bdb14d65fe20d8cdc
4
+ data.tar.gz: b2623750accd4178e7d938ebf87abbb6e1d40f14d4e252b9654f015d4141ce3c
5
5
  SHA512:
6
- metadata.gz: 5d6aba7952cc495dd4b74ea0aa452f473e01dc6a6bda2a94d1238d9102f5b448de2b4ae1765f87e2020d2fb460b1613044fe9403f0019b54b050b3a48261b425
7
- data.tar.gz: 21ddb1d302b53049a605052d67bbdf2d51bee9a4e80f82c6c10f689d5b292397e0d913dae4b05e66c93ee4c93d831e14e885c448a77b049dab48ac1bda3b84b5
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
- def resolve_value(val)
199
- val.respond_to?(:call) ? val.call : val
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
@@ -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).check(identifier, cost: cost)
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gitlab-labkit
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Newdigate