keycloak-sdk 1.3.1 → 1.3.2

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: 561abaa5612faf7d8d0b60dda6e925f2db242ae134e0fe68f97469ce3456ab26
4
- data.tar.gz: 1b2ff3c20ae257420c10a0d87cca3298059aa1f4c716c0c2179874eb0550f1ab
3
+ metadata.gz: 51b3c59bfdc3c281f3a6b71faaa6fba7e9561399bfa9e352cf275cbc42718617
4
+ data.tar.gz: 3260c90043c5d10f253266af8fc3786c7212906b1b460bfd05eb1e49cb0e38ed
5
5
  SHA512:
6
- metadata.gz: 2ec9e56d3bcc400a6ac1cd541e2e85e3ac31de448ec5ebec1a6f033c5e814ed145d9ab2d27543c0a68a74ce521d95e3227a1290d3d0c47933e53274b07f2b28c
7
- data.tar.gz: cf086c552fa47a7c5783850e0766a7590c47788d94b50ee27ebaaf913216490f4c8b1237fab715ac578b3160ad70f82d293b226a3301460d14fff6af0e077e0e
6
+ metadata.gz: 04b4477363357a3fdfa3a2c324dfdbd8a992c28a8ce163f84ac65fa6f6a0bfbf591a9261786a7251f91de1d3398b5f8b279be7f696f56e709009e8f1e308274e
7
+ data.tar.gz: da685f6729d4aa0362a9d71397c23aab3205b63aae49da6199b64348d8764d4c6988dfe0be9095d912f0fbd1eab9ab9899bebff999327a06bf851c94de31eccf
data/README.md CHANGED
@@ -4,7 +4,9 @@ Authentication (OIDC / OAuth2) and the Admin REST API for [Keycloak](https://www
4
4
 
5
5
  Part of a **nine-language polyglot SDK** (Java · Python · Node · Go · C# · PHP · Rust · Ruby · Kotlin) — one API surface, isomorphic across all of them: [github.com/xzawed/KeyCloakSDK](https://github.com/xzawed/KeyCloakSDK).
6
6
 
7
- > **`1.3.1` is on RubyGems** — a patch release on top of `1.3.0`; `1.0.0` was the first release carrying the stability guarantee. A bare `gem install keycloak-sdk` resolves it. **Fixed — upgrade if you installed `1.3.0` recently**: with net-http `0.10` (what a fresh install resolves), a response header name that holds a control character or is longer than 1,024 bytes made `introspect`, `logout` and the admin client raise a raw `ArgumentError` that quoted the name; they now raise `KeycloakSdk::TransportError` (`introspection transport error: ArgumentError` and the like — the cause chain keeps only the class name), as the grants already raised `AuthError`. **Security**: the response *framing* — status line, header lines, chunk-size and extension lines, trailers — was outside the body caps, so a 16 MiB header line, chunk extension or trailer line, or 4,096 trailer lines of 4 KiB, were read whole (32–67 MB allocated per call). Every connection the SDK makes now reads at most 8,192 bytes per framing line and 65,536 bytes of framing per response, and beyond that drops the connection with a constant-message `KeycloakSdk::TransportError` that does not quote the response; default Keycloak responses are far below both. The bound works with net-http `0.9` and `0.10`, and it touches only the sockets the SDK opens. ⚠️ The framing budget also counts chunk-size lines, so a large body cut into chunks of a few dozen bytes is refused (Keycloak and mainstream reverse proxies chunk in KiB and pass). The admin client now refuses, before any request, an access token an HTTP header cannot carry — NUL, DEL and other control characters, or a leading or trailing space or tab — with `AuthError`; net-http used to strip or forward them. ⚠️ A consumer `TokenProvider` whose token ends in a space or tab now fails instead of being trimmed. No public API change.
7
+ > **`1.3.2` is on RubyGems** — a patch release on top of `1.3.1`; `1.0.0` was the first release carrying the stability guarantee. A bare `gem install keycloak-sdk` resolves it. **Security**: an HTTPS call through a proxy (the SDK has no proxy option — Faraday reads `https_proxy`/`HTTPS_PROXY`) read the proxy's CONNECT reply outside the framing caps, because net-http reads that reply into its own buffer before the point the SDK hooks: net-http `0.9.1` read a 100,000-byte header line, 400 short header lines (82,039 bytes) or a 996,039-byte reply whole and went on to the TLS handshake with the origin, and `0.10.0` read the status line and header block up to its own 1 MiB limit. The socket that carries the CONNECT reply — the TCP socket for a plain proxy, the TLS socket with the proxy for a TLS proxy, never the TLS session with the origin — now gets the same two caps (8,192 bytes per line, 65,536 bytes per header block) and the same constant-message `KeycloakSdk::TransportError`; a well-formed reply passes as before. ⚠️ **`net-http >= 0.9` is now a runtime dependency** — the cap hooks `Net::HTTP#timeouted_connect`, which first appears in `0.9.0`. faraday-net_http already pulls net-http in, so most installs change nothing, but an application that pins net-http below `0.9` must raise it to install this release. **Fixed**: an admin error status (404, 409, 403 or any other non-2xx) whose body claims `Content-Type: application/json` but is not JSON — a proxy's HTML error page, say — raised `TransportError` (`admin transport error: Faraday::ParsingError`) instead of the status class; it now raises `NotFoundError`, `ConflictError`, `ForbiddenError` or `AdminError` with the usual `admin request failed: HTTP <status>` and quotes the body neither in the message nor in the cause chain (a 2xx that does not parse is still a `TransportError`). ⚠️ Code that rescued `KeycloakSdk::TransportError` for such responses now gets the status class. No public API change.
8
+ >
9
+ > `1.3.1` was a patch release on top of `1.3.0`. **Fixed — upgrade if you installed `1.3.0` recently**: with net-http `0.10` (what a fresh install resolves), a response header name that holds a control character or is longer than 1,024 bytes made `introspect`, `logout` and the admin client raise a raw `ArgumentError` that quoted the name; they now raise `KeycloakSdk::TransportError` (`introspection transport error: ArgumentError` and the like — the cause chain keeps only the class name), as the grants already raised `AuthError`. **Security**: the response *framing* — status line, header lines, chunk-size and extension lines, trailers — was outside the body caps, so a 16 MiB header line, chunk extension or trailer line, or 4,096 trailer lines of 4 KiB, were read whole (32–67 MB allocated per call). Every connection the SDK makes now reads at most 8,192 bytes per framing line and 65,536 bytes of framing per response, and beyond that drops the connection with a constant-message `KeycloakSdk::TransportError` that does not quote the response; default Keycloak responses are far below both. The bound works with net-http `0.9` and `0.10`, and it touches only the sockets the SDK opens. ⚠️ The framing budget also counts chunk-size lines, so a large body cut into chunks of a few dozen bytes is refused (Keycloak and mainstream reverse proxies chunk in KiB and pass). The admin client now refuses, before any request, an access token an HTTP header cannot carry — NUL, DEL and other control characters, or a leading or trailing space or tab — with `AuthError`; net-http used to strip or forward them. ⚠️ A consumer `TokenProvider` whose token ends in a space or tab now fails instead of being trimmed. No public API change.
8
10
 
9
11
  ## Requirements
10
12
 
@@ -36,9 +36,26 @@ module KeycloakSdk
36
36
  rescue StandardError => e
37
37
  # 원본을 달면 `Faraday::ParsingError#inspect` 가 요청의 베어러 토큰을, 메시지가 응답 앞부분을 찍는다(실측).
38
38
  # Faraday 밖 예외도 감싼다 — net-http 0.10 의 응답 헤더 검증 ArgumentError 는 헤더 이름을 인용한다.
39
+ status = unparsable_error_status(e)
40
+ if status
41
+ raise AdminError.from_status(status, "admin request failed: HTTP #{status}"), cause: RedactedCause.new(e)
42
+ end
43
+
39
44
  raise TransportError, "admin transport error: #{RedactedCause.describe(e)}", cause: RedactedCause.new(e)
40
45
  end
41
46
 
47
+ # 커넥션의 json 미들웨어는 상태 검사보다 먼저 해석한다 — JSON 이라면서 JSON 이 아닌 **오류** 본문(404·409·403 …)이어도
48
+ # 상태가 클래스를 정한다(.NET 과 같다). 2xx 의 해석 실패는 전송 오류 그대로다(nil).
49
+ # ⚠️ `Faraday::Error#response_status` 를 쓰지 않는다 — faraday 2.0.0 은 응답 객체에 `[:status]` 를 묻고, 그것은
50
+ # `headers[:status]` 로 위임돼 nil 이다. 응답 객체면 `status`, 해시면 `[:status]` 를 읽는다.
51
+ def unparsable_error_status(err)
52
+ return unless err.is_a?(Faraday::ParsingError)
53
+
54
+ resp = err.response
55
+ status = resp.respond_to?(:status) ? resp.status : (resp[:status] if resp.is_a?(Hash))
56
+ status if status.is_a?(Integer) && !(200..299).cover?(status)
57
+ end
58
+
42
59
  def id_from_location(resp)
43
60
  loc = resp.headers["location"]
44
61
  loc&.split("/")&.last
@@ -146,6 +146,73 @@ module KeycloakSdk
146
146
  end
147
147
  end
148
148
 
149
+ # ⚠️ **프록시 CONNECT 응답의 틀 상한** — HTTPS 대상을 프록시로 부르면 net-http `connect` 는 프록시 소켓 위에 **지역**
150
+ # `BufferedIO` 를 따로 만들어 CONNECT 응답(`HTTPResponse.read_new`)을 읽는다. `on_connect` 보다 앞이라 `BoundedReads` 가
151
+ # 그 읽기를 못 봤다(ruby-framing-proxy-connect-unbounded — net-http 0.9.1 은 996,039 B 머리 블록도 받고 TLS 로
152
+ # 넘어갔고, 0.10.0 은 상태 줄·블록을 1 MiB 까지 받았다 · 65,536 B 넘는 헤더 값만 다 읽은 뒤 raw ArgumentError).
153
+ # 그 `BufferedIO` 는 잡을 자리가 없으니(전역 패치 금지) 그것이 읽는 **소켓 하나**에 건다 — `rbuf_fill` 이 부르는
154
+ # `read_nonblock` 이 돌려준 바이트를 세어 `BoundedReads` 와 같은 두 상한(줄은 종단 문자까지 8,192 · 블록은 빈 줄까지
155
+ # 65,536)을 넘으면 같은 상수 메시지의 `TransportError` 다(`connect` 의 rescue 가 그 소켓을 닫는다). 빈 줄(첫 줄 뒤의
156
+ # "\r\n"·"\n" — 두 읽기에 갈려도)에서 꺼진다: 그 뒤 바이트는 손대지 않고 돌려주고(net-http 가 버린다) 그 소켓의 뒤
157
+ # 읽기는 세지 않는다.
158
+ # ⚠️ 빈 줄은 "\r\n"·"\n" 둘뿐이다(0.10 의 `chomp` 와 같다) — 0.9.1 처럼 공백만 있는 줄까지 끝으로 보면 그 줄 하나로
159
+ # 상한을 끌 수 있다.
160
+ module ConnectFraming
161
+ # 시그니처는 상류(`IO`·`OpenSSL::Buffering`)와 같다 — `BufferedIO#rbuf_fill` 은 `read_nonblock(BUFSIZE, tmp,
162
+ # exception: false)` 로 부르고(`http_framing_pins_spec`), 받은 버퍼를 같은 객체로 돌려받기를 기대한다.
163
+ def read_nonblock(len, buf = nil, exception: true)
164
+ chunk = super
165
+ kcsdk_connect_scan(chunk) if @kcsdk_connect_armed && chunk.is_a?(String)
166
+ chunk
167
+ end
168
+
169
+ def kcsdk_arm_connect_framing!
170
+ @kcsdk_connect_armed = true
171
+ @kcsdk_connect_total = 0 # 끝난 줄들의 바이트(종단 문자 포함)
172
+ @kcsdk_connect_line = 0 # 지금 줄에 쌓인 바이트 — 아직 "\n" 전
173
+ @kcsdk_connect_last = nil # 그 쌓인 바이트의 마지막 — "\r" 하나로 읽기가 끝났으면 다음 읽기의 "\n" 과 빈 줄이다
174
+ end
175
+
176
+ def kcsdk_connect_framing_armed?
177
+ @kcsdk_connect_armed == true
178
+ end
179
+
180
+ private
181
+
182
+ # ⚠️ 바이트 위치로 읽는다(`byteindex`·`getbyte`) — 소켓이 돌려주는 BINARY 문자열에서 위치는 늘 바이트다.
183
+ def kcsdk_connect_scan(chunk)
184
+ from = 0
185
+ while @kcsdk_connect_armed && (eol = chunk.byteindex("\n", from))
186
+ kcsdk_connect_line_end(chunk, from, eol)
187
+ from = eol + 1
188
+ end
189
+ kcsdk_connect_partial(chunk, from) if @kcsdk_connect_armed && from < chunk.bytesize
190
+ end
191
+
192
+ # 빈 줄 = 첫 줄이 아닌 "\n"(1 바이트) 또는 "\r\n"(2 바이트) — 2 바이트 줄의 "\r" 은 "\n" 바로 앞 바이트다.
193
+ def kcsdk_connect_line_end(chunk, from, eol)
194
+ before = eol > from ? chunk.getbyte(eol - 1) : @kcsdk_connect_last
195
+ len = @kcsdk_connect_line + eol + 1 - from
196
+ kcsdk_connect_bound(len)
197
+ blank = @kcsdk_connect_total.positive? && (len == 1 || (len == 2 && before == 13))
198
+ @kcsdk_connect_total += len
199
+ @kcsdk_connect_line = 0
200
+ @kcsdk_connect_armed = false if blank
201
+ end
202
+
203
+ def kcsdk_connect_partial(chunk, from)
204
+ @kcsdk_connect_line += chunk.bytesize - from
205
+ @kcsdk_connect_last = chunk.getbyte(-1)
206
+ kcsdk_connect_bound(@kcsdk_connect_line)
207
+ end
208
+
209
+ # `BoundedReads#kcsdk_bound_line` 와 같은 경계다 — 줄이든 블록이든 상한과 **같으면** 받고 넘으면 거부한다.
210
+ def kcsdk_connect_bound(len)
211
+ raise KeycloakSdk::TransportError, FRAMING_LINE_MSG if len > MAX_FRAMING_LINE_BYTES
212
+ raise KeycloakSdk::TransportError, FRAMING_TOTAL_MSG if @kcsdk_connect_total + len > MAX_FRAMING_BYTES
213
+ end
214
+ end
215
+
149
216
  # `Net::HTTP` 인스턴스의 싱글턴에 **prepend** 해 소켓이 만들어지는 자리(`on_connect`, `connect` 끝)에서 그 소켓에만
150
217
  # `BoundedReads` 를 건다. `begin_transport` 는 요청마다 틀 예산을 0 으로 되돌린다 — keep-alive 로 연결을 재사용해도
151
218
  # 다음 응답은 새 예산을 받는다(한 요청의 1xx 중간 응답들은 같은 예산을 나눠 쓴다 — 그래야 1xx 홍수도 묶인다).
@@ -167,6 +234,37 @@ module KeycloakSdk
167
234
  super
168
235
  @socket.kcsdk_reset_framing! if @socket.respond_to?(:kcsdk_reset_framing!)
169
236
  end
237
+
238
+ # ── 프록시 CONNECT(`ConnectFraming`) — `connect` 가 소켓을 여는 두 자리. 상한은 CONNECT 응답을 나르는 소켓에만 걸고
239
+ # 대상과의 TLS 소켓은 건드리지 않는다. ⚠️ `timeouted_connect` 는 net-http 0.9.0 에 생겼다 — gemspec 하한이다.
240
+
241
+ # 연결마다 「이 `connect` 의 첫 `ssl_socket_connect`」를 되돌린다(keep-alive 가 끊긴 뒤 다시 열 때도 같다).
242
+ def connect
243
+ @kcsdk_connect_tls_pending = true
244
+ super
245
+ end
246
+
247
+ # 평문 프록시 + HTTPS 대상 — net-http 는 이 TCP 소켓을 그대로 BufferedIO 로 감싸 CONNECT 응답을 읽는다.
248
+ def timeouted_connect(conn_addr, conn_port)
249
+ sock = super
250
+ kcsdk_bound_connect(sock) if proxy? && use_ssl? && !@proxy_use_ssl
251
+ sock
252
+ end
253
+
254
+ # TLS 프록시 + HTTPS 대상 — 이 `connect` 의 **첫** 호출이 프록시와의 TLS 소켓(CONNECT 응답을 나른다)이고, 둘째는
255
+ # 대상과의 TLS 다. `@proxy_use_ssl` 은 `connect` 가 가르는 그 ivar 다(공개 읽기가 없다 — 모양은 핀으로 고정).
256
+ def ssl_socket_connect(sock, timeout)
257
+ first = @kcsdk_connect_tls_pending
258
+ @kcsdk_connect_tls_pending = false
259
+ result = super
260
+ kcsdk_bound_connect(sock) if first && proxy? && use_ssl? && @proxy_use_ssl
261
+ result
262
+ end
263
+
264
+ def kcsdk_bound_connect(sock)
265
+ sock.extend(ConnectFraming)
266
+ sock.kcsdk_arm_connect_framing!
267
+ end
170
268
  end
171
269
 
172
270
  # 표준 `:net_http` 어댑터의 설정 블록 — 커넥션을 짓기 직전 그 `Net::HTTP` 인스턴스의 싱글턴에 `BoundedTransport` 를
@@ -175,7 +273,7 @@ module KeycloakSdk
175
273
  BOUND_CONNECTION = lambda do |http|
176
274
  http.singleton_class.prepend(BoundedTransport)
177
275
  end
178
- private_constant :BoundedReads, :BoundedTransport, :BOUND_CONNECTION
276
+ private_constant :BoundedReads, :ConnectFraming, :BoundedTransport, :BOUND_CONNECTION
179
277
 
180
278
  module_function
181
279
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module KeycloakSdk
4
- VERSION = "1.3.1"
4
+ VERSION = "1.3.2"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: keycloak-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.1
4
+ version: 1.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - xzawed
@@ -37,6 +37,20 @@ dependencies:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: '3.2'
40
+ - !ruby/object:Gem::Dependency
41
+ name: net-http
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0.9'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0.9'
40
54
  - !ruby/object:Gem::Dependency
41
55
  name: rack-oauth2
42
56
  requirement: !ruby/object:Gem::Requirement