Skip to content

Commit 06467f9

Browse files
authored
toil(auth): switch to battle tested RemoteIp in IpFilter (#475)
## πŸ“ Description Use RemoteIp instead to get the remote ip of an customer, same way that we are doing that in the other parts of the auth service. ## βœ… Checklist - [x] I have tested this change - [x] ~This change requires documentation update~
1 parent af55fa3 commit 06467f9

File tree

8 files changed

+36
-187
lines changed

8 files changed

+36
-187
lines changed

β€Žauth/lib/auth.ex

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ defmodule Auth do
115115
!org ->
116116
send_resp(conn, 401, "Unauthorized")
117117

118-
Auth.IpFilter.block?(conn, org) ->
118+
Auth.IpFilter.block?(conn.remote_ip, org) ->
119119
send_resp(conn, 404, blocked_ip_response(conn))
120120

121121
true ->
@@ -326,7 +326,7 @@ defmodule Auth do
326326
!org ->
327327
{:error, :missing_organization, conn}
328328

329-
Auth.IpFilter.block?(conn, org) ->
329+
Auth.IpFilter.block?(conn.remote_ip, org) ->
330330
{:error, :unauthorized_ip, conn}
331331

332332
true ->
@@ -387,7 +387,7 @@ defmodule Auth do
387387
!org ->
388388
{:error, :missing_organization, conn}
389389

390-
Auth.IpFilter.block?(conn, org) ->
390+
Auth.IpFilter.block?(conn.remote_ip, org) ->
391391
{:error, :unauthorized_ip, conn}
392392

393393
true ->
@@ -416,7 +416,7 @@ defmodule Auth do
416416
!org ->
417417
{:error, :missing_organization, conn}
418418

419-
Auth.IpFilter.block?(conn, org) ->
419+
Auth.IpFilter.block?(conn.remote_ip, org) ->
420420
{:error, :unauthorized_ip, conn}
421421

422422
true ->
@@ -680,7 +680,7 @@ defmodule Auth do
680680
end
681681

682682
defp blocked_ip_response(conn) do
683-
ip = Auth.IpFilter.client_ip(conn) |> Tuple.to_list() |> Enum.join(".")
683+
ip = conn.remote_ip |> :inet.ntoa()
684684

685685
"""
686686
You cannot access this organization from your current IP address (#{ip}) due to the security settings enabled by the organization administrator.

β€Žauth/lib/auth/ip_filter.ex

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,14 @@
11
defmodule Auth.IpFilter do
22
require Logger
33

4-
def block?(conn, org) do
4+
def block?(client_ip, org) do
55
if Enum.empty?(org.ip_allow_list) do
66
false
77
else
8-
_block?(client_ip(conn), org.ip_allow_list)
8+
_block?(client_ip, org.ip_allow_list)
99
end
1010
end
1111

12-
def client_ip(conn) do
13-
Plug.Conn.get_req_header(conn, "x-forwarded-for")
14-
|> List.last()
15-
|> String.split(", ")
16-
|> List.first()
17-
|> InetCidr.parse_address!()
18-
rescue
19-
e ->
20-
Watchman.increment("auth.ip_filter.error")
21-
Logger.error("Error parsing client IP: #{inspect(e)}")
22-
Logger.error("Headers: #{inspect(conn.req_headers)}")
23-
24-
# If something goes wrong here, it means Ingress/Ambassador are sending us
25-
# a bad X-Forwarded-For header, which is very unlikely, so we fail open
26-
nil
27-
end
28-
2912
defp _block?(nil, _), do: false
3013

3114
defp _block?(client_ip, ip_allow_list) do

β€Žauth/mix.lock

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,19 @@
2121
"jumper": {:hex, :jumper, "1.0.1", "3c00542ef1a83532b72269fab9f0f0c82bf23a35e27d278bfd9ed0865cecabff", [:mix], [], "hexpm", "318c59078ac220e966d27af3646026db9b5a5e6703cb2aa3e26bcfaba65b7433"},
2222
"junit_formatter": {:hex, :junit_formatter, "3.3.1", "c729befb848f1b9571f317d2fefa648e9d4869befc4b2980daca7c1edc468e40", [:mix], [], "hexpm", "761fc5be4b4c15d8ba91a6dafde0b2c2ae6db9da7b8832a55b5a1deb524da72b"},
2323
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
24-
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
24+
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
2525
"mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"},
2626
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
27-
"plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"},
27+
"plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"},
2828
"plug_cowboy": {:hex, :plug_cowboy, "2.5.2", "62894ccd601cf9597e2c23911ff12798a8a18d237e9739f58a6b04e4988899fe", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ea6e87f774c8608d60c8d34022a7d073bd7680a0a013f049fc62bf35efea1044"},
29-
"plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"},
29+
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
3030
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"},
3131
"protobuf": {:hex, :protobuf, "0.7.1", "7d1b9f7d9ecb32eccd96b0c58572de4d1c09e9e3bc414e4cb15c2dce7013f195", [:mix], [], "hexpm", "6eff7a5287963719521c82e5d5b4583fd1d7cdd89ad129f0ea7d503a50a4d13f"},
3232
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
33-
"remote_ip": {:hex, :remote_ip, "1.1.0", "cb308841595d15df3f9073b7c39243a1dd6ca56e5020295cb012c76fbec50f2d", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "616ffdf66aaad6a72fc546dabf42eed87e2a99e97b09cbd92b10cc180d02ed74"},
33+
"remote_ip": {:hex, :remote_ip, "1.2.0", "fb078e12a44414f4cef5a75963c33008fe169b806572ccd17257c208a7bc760f", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "2ff91de19c48149ce19ed230a81d377186e4412552a597d6a5137373e5877cb7"},
3434
"sentry": {:hex, :sentry, "8.0.6", "c8de1bf0523bc120ec37d596c55260901029ecb0994e7075b0973328779ceef7", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 2.3", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "051a2d0472162f3137787c7c9d6e6e4ef239de9329c8c45b1f1bf1e9379e1883"},
3535
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
36-
"telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"},
36+
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
3737
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
3838
"unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm", "6c7729a2d214806450d29766abc2afaa7a2cbecf415be64f36a6691afebb50e5"},
3939
"uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm", "c790593b4c3b601f5dc2378baae7efaf5b3d73c4c6456ba85759905be792f2ac"},

β€Žauth/test/auth/cache_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
defmodule Auth.CacheTest do
22
use ExUnit.Case
3-
use Plug.Test
3+
44
doctest Auth
55

66
test "it caches values" do

β€Žauth/test/auth/cli_test.exs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
defmodule Auth.CliTest do
22
use ExUnit.Case
3-
use Plug.Test
3+
4+
import Plug.Test
5+
import Plug.Conn
46

57
describe ".is_call_from_deprecated_cli?" do
68
test "no user agent => returns false" do

β€Žauth/test/auth/ip_filter_test.exs

Lines changed: 13 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -1,248 +1,107 @@
11
defmodule Auth.IpFilterTest do
22
use ExUnit.Case
3-
use Plug.Test
43

54
@org_id UUID.uuid4()
65

76
describe "#block?" do
87
test "empty ip_allow_list => returns false" do
9-
conn = conn(:get, "https://org1.semaphoretest.test/exauth/api/v1alpha/jobs")
10-
11-
refute Auth.IpFilter.block?(conn, %{
8+
refute Auth.IpFilter.block?({172, 14, 101, 99}, %{
129
id: @org_id,
1310
name: "semaphore",
1411
ip_allow_list: []
1512
})
1613
end
1714

18-
test "no X-Forwarded-For header => returns false" do
19-
conn = conn(:get, "https://org1.semaphoretest.test/exauth/api/v1alpha/jobs")
20-
21-
refute Auth.IpFilter.block?(conn, %{
22-
id: @org_id,
23-
name: "semaphore",
24-
ip_allow_list: ["172.14.101.99"]
25-
})
26-
end
27-
28-
test "bad X-Forwarded-For header => returns false" do
29-
conn =
30-
Plug.Adapters.Test.Conn.conn(
31-
%Plug.Conn{req_headers: [{"x-forwarded-for", "999.999.999.999"}]},
32-
:get,
33-
"https://org1.semaphoretest.test/exauth/api/v1alpha/jobs",
34-
nil
35-
)
36-
37-
refute Auth.IpFilter.block?(conn, %{
38-
id: @org_id,
39-
name: "semaphore",
40-
ip_allow_list: ["172.14.101.99"]
41-
})
42-
end
43-
44-
test "single bad IP => returns false" do
45-
conn =
46-
Plug.Adapters.Test.Conn.conn(
47-
%Plug.Conn{req_headers: [{"x-forwarded-for", "172.14.101.99"}]},
48-
:get,
49-
"https://org1.semaphoretest.test/exauth/api/v1alpha/jobs",
50-
nil
51-
)
52-
53-
refute Auth.IpFilter.block?(conn, %{
54-
id: @org_id,
55-
name: "semaphore",
56-
ip_allow_list: ["999.999.999.999"]
57-
})
58-
end
59-
6015
test "single IP => returns false if request comes from the same IP" do
61-
conn =
62-
Plug.Adapters.Test.Conn.conn(
63-
%Plug.Conn{req_headers: [{"x-forwarded-for", "172.14.101.99"}]},
64-
:get,
65-
"https://org1.semaphoretest.test/exauth/api/v1alpha/jobs",
66-
nil
67-
)
68-
69-
refute Auth.IpFilter.block?(conn, %{
16+
refute Auth.IpFilter.block?({172, 14, 101, 99}, %{
7017
id: @org_id,
7118
name: "semaphore",
7219
ip_allow_list: ["172.14.101.99"]
7320
})
7421
end
7522

7623
test "single IP => returns true if request does not come from the same IP" do
77-
conn =
78-
Plug.Adapters.Test.Conn.conn(
79-
%Plug.Conn{req_headers: [{"x-forwarded-for", "211.191.11.4"}]},
80-
:get,
81-
"https://org1.semaphoretest.test/exauth/api/v1alpha/jobs",
82-
nil
83-
)
84-
85-
assert Auth.IpFilter.block?(conn, %{
24+
assert Auth.IpFilter.block?({211, 191, 11, 4}, %{
8625
id: @org_id,
8726
name: "semaphore",
8827
ip_allow_list: ["172.14.101.99"]
8928
})
9029
end
9130

9231
test "multiple IPs => returns false if request comes from one of the IPs allowed" do
93-
conn =
94-
Plug.Adapters.Test.Conn.conn(
95-
%Plug.Conn{req_headers: [{"x-forwarded-for", "172.14.101.99"}]},
96-
:get,
97-
"https://org1.semaphoretest.test/exauth/api/v1alpha/jobs",
98-
nil
99-
)
100-
101-
refute Auth.IpFilter.block?(conn, %{
32+
refute Auth.IpFilter.block?({172, 14, 101, 99}, %{
10233
id: @org_id,
10334
name: "semaphore",
10435
ip_allow_list: ["32.109.221.12", "172.14.101.99"]
10536
})
10637
end
10738

10839
test "multiple IPs => returns true if request comes from none of the IPs allowed" do
109-
conn =
110-
Plug.Adapters.Test.Conn.conn(
111-
%Plug.Conn{req_headers: [{"x-forwarded-for", "211.191.11.4"}]},
112-
:get,
113-
"https://org1.semaphoretest.test/exauth/api/v1alpha/jobs",
114-
nil
115-
)
116-
117-
assert Auth.IpFilter.block?(conn, %{
40+
assert Auth.IpFilter.block?({211, 191, 11, 4}, %{
11841
id: @org_id,
11942
name: "semaphore",
12043
ip_allow_list: ["32.109.221.12", "172.14.101.99"]
12144
})
12245
end
12346

12447
test "single bad CIDR => returns false" do
125-
conn =
126-
Plug.Adapters.Test.Conn.conn(
127-
%Plug.Conn{req_headers: [{"x-forwarded-for", "172.14.101.99"}]},
128-
:get,
129-
"https://org1.semaphoretest.test/exauth/api/v1alpha/jobs",
130-
nil
131-
)
132-
133-
refute Auth.IpFilter.block?(conn, %{
48+
refute Auth.IpFilter.block?({172, 14, 101, 99}, %{
13449
id: @org_id,
13550
name: "semaphore",
13651
ip_allow_list: ["32.109.221.12/999"]
13752
})
13853
end
13954

14055
test "single CIDR => returns false if request comes from IP inside CIDR" do
141-
conn =
142-
Plug.Adapters.Test.Conn.conn(
143-
%Plug.Conn{req_headers: [{"x-forwarded-for", "32.109.221.1"}]},
144-
:get,
145-
"https://org1.semaphoretest.test/exauth/api/v1alpha/jobs",
146-
nil
147-
)
148-
149-
refute Auth.IpFilter.block?(conn, %{
56+
refute Auth.IpFilter.block?({32, 109, 221, 1}, %{
15057
id: @org_id,
15158
name: "semaphore",
15259
ip_allow_list: ["32.109.221.12/28"]
15360
})
15461
end
15562

15663
test "single CIDR => returns true if request comes from IP outside the CIDR" do
157-
conn =
158-
Plug.Adapters.Test.Conn.conn(
159-
%Plug.Conn{req_headers: [{"x-forwarded-for", "32.109.222.1"}]},
160-
:get,
161-
"https://org1.semaphoretest.test/exauth/api/v1alpha/jobs",
162-
nil
163-
)
164-
165-
assert Auth.IpFilter.block?(conn, %{
64+
assert Auth.IpFilter.block?({32, 109, 222, 1}, %{
16665
id: @org_id,
16766
name: "semaphore",
16867
ip_allow_list: ["32.109.221.12/28"]
16968
})
17069
end
17170

17271
test "multiple CIDRs => returns false if request comes from IP inside one of the CIDRs" do
173-
conn =
174-
Plug.Adapters.Test.Conn.conn(
175-
%Plug.Conn{req_headers: [{"x-forwarded-for", "32.109.221.1"}]},
176-
:get,
177-
"https://org1.semaphoretest.test/exauth/api/v1alpha/jobs",
178-
nil
179-
)
180-
181-
refute Auth.IpFilter.block?(conn, %{
72+
refute Auth.IpFilter.block?({32, 109, 221, 1}, %{
18273
id: @org_id,
18374
name: "semaphore",
18475
ip_allow_list: ["113.51.211.0/16", "32.109.221.12/28"]
18576
})
18677
end
18778

18879
test "multiple CIDRs => returns true if request comes from IP outside all CIDRs" do
189-
conn =
190-
Plug.Adapters.Test.Conn.conn(
191-
%Plug.Conn{req_headers: [{"x-forwarded-for", "45.111.201.7"}]},
192-
:get,
193-
"https://org1.semaphoretest.test/exauth/api/v1alpha/jobs",
194-
nil
195-
)
196-
197-
assert Auth.IpFilter.block?(conn, %{
80+
assert Auth.IpFilter.block?({45, 111, 201, 7}, %{
19881
id: @org_id,
19982
name: "semaphore",
20083
ip_allow_list: ["113.51.211.0/16", "32.109.221.12/28"]
20184
})
20285
end
20386

20487
test "IP + CIDR => returns false if request comes from IP inside CIDR" do
205-
conn =
206-
Plug.Adapters.Test.Conn.conn(
207-
%Plug.Conn{req_headers: [{"x-forwarded-for", "32.109.221.1"}]},
208-
:get,
209-
"https://org1.semaphoretest.test/exauth/api/v1alpha/jobs",
210-
nil
211-
)
212-
213-
refute Auth.IpFilter.block?(conn, %{
88+
refute Auth.IpFilter.block?({32, 109, 221, 1}, %{
21489
id: @org_id,
21590
name: "semaphore",
21691
ip_allow_list: ["113.51.211.12", "32.109.221.12/28"]
21792
})
21893
end
21994

22095
test "IP + CIDR => returns false if request comes from one of the allowed IPs" do
221-
conn =
222-
Plug.Adapters.Test.Conn.conn(
223-
%Plug.Conn{req_headers: [{"x-forwarded-for", "113.51.211.12"}]},
224-
:get,
225-
"https://org1.semaphoretest.test/exauth/api/v1alpha/jobs",
226-
nil
227-
)
228-
229-
refute Auth.IpFilter.block?(conn, %{
96+
refute Auth.IpFilter.block?({113, 51, 211, 12}, %{
23097
id: @org_id,
23198
name: "semaphore",
23299
ip_allow_list: ["113.51.211.12", "32.109.221.12/28"]
233100
})
234101
end
235102

236103
test "IP + CIDR => returns true if request comes from IP not in CIDRs and not in allowed IPs" do
237-
conn =
238-
Plug.Adapters.Test.Conn.conn(
239-
%Plug.Conn{req_headers: [{"x-forwarded-for", "35.121.222.37"}]},
240-
:get,
241-
"https://org1.semaphoretest.test/exauth/api/v1alpha/jobs",
242-
nil
243-
)
244-
245-
assert Auth.IpFilter.block?(conn, %{
104+
assert Auth.IpFilter.block?({35, 121, 222, 37}, %{
246105
id: @org_id,
247106
name: "semaphore",
248107
ip_allow_list: ["113.51.211.12", "32.109.221.12/28"]

β€Žauth/test/auth/refuse_x_semaphore_headers_test.exs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
defmodule Auth.RefuseXSemaphoreHeadersTest do
22
use ExUnit.Case
3-
use Plug.Test
3+
4+
import Plug.Test
5+
import Plug.Conn
46

57
test "the caller passed an x-semaphore-* header, we respond with 404" do
68
assert {404, _, "Not Found"} = call_with_header("x-semaphore-user-id", "some-value")

0 commit comments

Comments
Β (0)