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 +4 -4
- data/README.md +3 -1
- data/lib/keycloak_sdk/admin/call.rb +17 -0
- data/lib/keycloak_sdk/http.rb +99 -1
- data/lib/keycloak_sdk/version.rb +1 -1
- metadata +15 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 51b3c59bfdc3c281f3a6b71faaa6fba7e9561399bfa9e352cf275cbc42718617
|
|
4
|
+
data.tar.gz: 3260c90043c5d10f253266af8fc3786c7212906b1b460bfd05eb1e49cb0e38ed
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
data/lib/keycloak_sdk/http.rb
CHANGED
|
@@ -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
|
|
data/lib/keycloak_sdk/version.rb
CHANGED
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.
|
|
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
|