gitlab-labkit 4.3.0 → 4.4.1

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: 63bc67bcb0041f919748393e6ace8e96d29bcd88e2c5b0e08b8baad8968670b1
4
- data.tar.gz: 5bd9b55ece983433e125c7cd087030d740647101a48e9af1f97f1aa68ed51db4
3
+ metadata.gz: 1e7ab6ac58c584a113159e7f42678f508078c83cdeb9a21c86a7f60a5c936b21
4
+ data.tar.gz: b4c3abaa02eafb546284dc68a08ba4443da4c1b13aa824c98f51e0dac3e749da
5
5
  SHA512:
6
- metadata.gz: 61d298ae9cb7c584d6e9979fbdb25fc112b705a8f1c0022825192e2759356408cbe6c03caf339ae5140220e325dc90532f24775a1f76decf1eb1547269cf3aba
7
- data.tar.gz: 3755c6de56d994d081210d1cae48430d9938420c6ac33932ce672f3c20eee86457742ce75966bbd2163c09807a4b4e4c72e82579f8eba262e25cb1b79204ce1f
6
+ metadata.gz: 1b032aa2876124669bf1eac1bc09cf67e79b4df4105e95fbc7d416132dd8d5bf1088c49e4c88b304e9a796e119edea60e4c2fae49a70aba050889a928b66b206
7
+ data.tar.gz: 91066213316a173023257e30507fdb8c7c0e4e0b360b91dd1efcafebbacd291c82f62e9470798aa86488ae991bb8f81b1c395856ee2e03b537e3ae5a244d78c3
data/.gitlab/CODEOWNERS CHANGED
@@ -1 +1 @@
1
- * @reprazent @andrewn @mkaeppler @ayufan @hmerscher @d.barrett @splattael
1
+ * @reprazent @andrewn @mkaeppler @ayufan @hmerscher @d.barrett @splattael @sankalp_gl @hardikgala @nindurkar @ashs2
data/CODEOWNERS CHANGED
@@ -1,4 +1,4 @@
1
1
  # CODEOWNERS is used to lookup assignees for
2
2
  # Renovate Bot dependency change Merge Requests.
3
3
  # https://docs.renovatebot.com/configuration-options/#assigneesfromcodeowners
4
- * @reprazent @andrewn @mkaeppler @ayufan @hmerscher @d.barrett @splattael @e_forbes @M_Alvarez @mwoolf
4
+ * @reprazent @andrewn @mkaeppler @ayufan @hmerscher @d.barrett @splattael @e_forbes @M_Alvarez @mwoolf @sankalp_gl @hardikgala @nindurkar @ashs2
@@ -174,10 +174,17 @@ A `match` hash gates whether a rule applies. Each value is normalised through
174
174
  | `Regexp` | `re` | `match: { endpoint: %r{\A/api/} }` |
175
175
  | `{ eq: <value> }` | `eq` | `match: { method: { eq: "POST" } }` (YAML-friendly) |
176
176
  | `{ re: <source> }`| `re` | `match: { endpoint: { re: '\A/api/' } }` (YAML-friendly) |
177
+ | `{ oneOf: <Array> }` | `oneOf` | `match: { user: { oneOf: ["7", "42"] } }` (YAML-friendly) |
177
178
 
178
179
  `re` coerces the identifier value via `#to_s` before matching, so it can be
179
180
  used against non-String values (e.g. matching a 503 status against `{ re: '^5' }`).
180
181
 
182
+ `oneOf` is set membership via `Set#include?` (`eql?`/`hash` equality, no
183
+ coercion): it agrees with `eq` for Strings, Symbols, booleans, nil, and
184
+ same-class numerics, but cross-type numerics differ (`1 == 1.0`, yet
185
+ `{ oneOf: [1] }` does not match `1.0`). A bare Array is rejected; membership must
186
+ be spelled `{ oneOf: [...] }` explicitly.
187
+
181
188
  Glob and prefix matchers are intentionally out of scope.
182
189
 
183
190
  ### Evaluation flow
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
4
+
3
5
  module Labkit
4
6
  module RateLimit
5
7
  # Matcher is the internal representation of a single key/value predicate in
@@ -11,6 +13,14 @@ module Labkit
11
13
  # - a Regexp instance -> :re matcher (Ruby convenience)
12
14
  # - { eq: <value> } -> :eq matcher (canonical, YAML-compatible)
13
15
  # - { re: <String|Regexp> } -> :re matcher (canonical, YAML-compatible)
16
+ # - { oneOf: <Array> } -> :oneOf matcher (set membership, YAML-compatible)
17
+ #
18
+ # A bare Array is rejected rather than treated as :oneOf, so a value that
19
+ # was meant as a single matcher hash but arrived as an Array fails loudly
20
+ # instead of silently becoming a membership test.
21
+ #
22
+ # :oneOf membership is Set#include? (eql?/hash equality), not :eq's ==, so
23
+ # cross-type numerics differ (1 == 1.0, but {oneOf: [1]} does not match 1.0).
14
24
  #
15
25
  # Hash-key naming follows the metrics-catalog selector pattern. Glob,
16
26
  # prefix, and other matcher kinds are intentionally out of scope here; see
@@ -20,7 +30,7 @@ module Labkit
20
30
  # the regex, so callers can match non-String identifier values such as
21
31
  # Integer status codes (e.g. {status: { re: "^5" }} against status: 503).
22
32
  class Matcher < Data.define(:type, :value)
23
- KNOWN_HASH_KEYS = %i[eq re].freeze
33
+ KNOWN_HASH_KEYS = %i[eq re oneOf].freeze
24
34
  MAX_REGEX_SOURCE_LENGTH = 200
25
35
  ERROR_INSPECT_LIMIT = 80
26
36
 
@@ -29,11 +39,13 @@ module Labkit
29
39
  # unbounded elsewhere), and a timeout reaches Evaluator's fail-open
30
40
  # rescue, so the request goes unlimited.
31
41
  #
32
- # 5ms is ~119x the slowest real match measured against GitLab's
33
- # RackAttack path patterns on inputs up to 16KB (worst: 0.0419ms, none
34
- # timed out). Re-measure if a rule ever needs nested quantifiers or
35
- # backreferences, which are what make backtracking exponential.
36
- MATCH_TIMEOUT_SECONDS = 0.005
42
+ # 50ms is a safety net against catastrophic backtracking, not a
43
+ # performance bound: real matches are far cheaper (median 0.087ms, worst
44
+ # 1.484ms across 6935 paths that timed out in production at 5ms). Those
45
+ # timeouts were wall-clock stalls, since Ruby counts real time and GC
46
+ # pauses on GitLab's git fleet have a p50 of ~72ms. Under 4x CPU
47
+ # oversubscription 50ms still fires on 0.16% of those matches, 5ms 1.33%.
48
+ MATCH_TIMEOUT_SECONDS = 0.05
37
49
 
38
50
  def self.build(input)
39
51
  case input
@@ -42,18 +54,14 @@ module Labkit
42
54
  when Hash
43
55
  from_hash(input)
44
56
  when Array
45
- raise ArgumentError,
46
- "rate-limit match value must be a single-key Hash like {re: \"...\"} or {eq: ...}, got #{truncate_for_error(input)}"
57
+ raise ArgumentError, invalid_shape_error(input)
47
58
  else
48
59
  new(type: :eq, value: input)
49
60
  end
50
61
  end
51
62
 
52
63
  def self.from_hash(input)
53
- if input.size != 1
54
- raise ArgumentError,
55
- "rate-limit match value must be a single-key Hash like {re: \"...\"} or {eq: ...}, got #{truncate_for_error(input)}"
56
- end
64
+ raise ArgumentError, invalid_shape_error(input) if input.size != 1
57
65
 
58
66
  type, source = input.first
59
67
  type_sym = type.to_sym
@@ -71,6 +79,13 @@ module Labkit
71
79
  case type_sym
72
80
  when :eq
73
81
  new(type: :eq, value: source)
82
+ when :oneOf
83
+ unless source.is_a?(Array)
84
+ raise ArgumentError,
85
+ "rate-limit match value {oneOf: ...} must be an Array, got #{truncate_for_error(source)}"
86
+ end
87
+
88
+ new(type: :oneOf, value: Set.new(source).freeze)
74
89
  when :re
75
90
  if source.to_s.length > MAX_REGEX_SOURCE_LENGTH
76
91
  raise ArgumentError,
@@ -113,6 +128,12 @@ module Labkit
113
128
 
114
129
  private_class_method :with_match_timeout
115
130
 
131
+ def self.invalid_shape_error(input)
132
+ "rate-limit match value must be a single-key Hash like {re: \"...\"}, {eq: ...} or {oneOf: [...]}, " \
133
+ "got #{truncate_for_error(input)}"
134
+ end
135
+ private_class_method :invalid_shape_error
136
+
116
137
  def self.truncate_for_error(value)
117
138
  s = value.inspect
118
139
  s.length > ERROR_INSPECT_LIMIT ? "#{s[0, ERROR_INSPECT_LIMIT]}...(truncated)" : s
@@ -123,6 +144,8 @@ module Labkit
123
144
  case type
124
145
  when :eq
125
146
  value == identifier_value
147
+ when :oneOf
148
+ value.include?(identifier_value)
126
149
  when :re
127
150
  value.match?(identifier_value.to_s)
128
151
  else
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: 4.3.0
4
+ version: 4.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Newdigate