Skip to content

Commit 8a53dce

Browse files
aleDszjonatanklosko
authored andcommitted
Keeps the user data while Livebook Teams is down (#3156)
1 parent fa50a7f commit 8a53dce

5 files changed

Lines changed: 127 additions & 20 deletions

File tree

lib/livebook/teams/requests.ex

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,9 +225,18 @@ defmodule Livebook.Teams.Requests do
225225
@doc """
226226
Send a request to Livebook Team API to get the user information from given access token.
227227
"""
228-
@spec get_user_info(Team.t(), String.t()) :: api_result()
228+
@spec get_user_info(Team.t(), String.t()) :: api_result() | :econnrefused
229229
def get_user_info(team, access_token) do
230-
get("/api/v1/org/identity", %{access_token: access_token}, team)
230+
req = build_req(team)
231+
params = %{access_token: access_token}
232+
233+
case Req.get(req, url: "/api/v1/org/identity", params: params) do
234+
{:error, %Req.TransportError{reason: :econnrefused}} ->
235+
:econnrefused
236+
237+
otherwise ->
238+
handle_response(otherwise)
239+
end
231240
end
232241

233242
@doc """
@@ -321,6 +330,17 @@ defmodule Livebook.Teams.Requests do
321330
|> Req.Request.put_new_header("x-lb-version", Livebook.Config.app_version())
322331
|> Livebook.Utils.req_attach_defaults()
323332
|> add_team_auth(team)
333+
|> put_test_req_options()
334+
end
335+
336+
if Mix.env() == :test do
337+
defp put_test_req_options(req) do
338+
Req.Request.merge_options(req, retry: false)
339+
end
340+
else
341+
defp put_test_req_options(req) do
342+
req
343+
end
324344
end
325345

326346
defp add_team_auth(req, nil), do: req

lib/livebook/zta/livebook_teams.ex

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,27 @@ defmodule Livebook.ZTA.LivebookTeams do
88

99
@behaviour NimbleZTA
1010

11-
@impl true
11+
@exp_timestamp_sec System.os_time(:second) + 3 * 3600
12+
13+
@impl NimbleZTA
1214
def child_spec(opts) do
1315
%{id: __MODULE__, start: {__MODULE__, :start_link, [opts]}}
1416
end
1517

1618
def start_link(opts) do
1719
name = Keyword.fetch!(opts, :name)
18-
identity_key = Keyword.fetch!(opts, :identity_key)
19-
team = Livebook.Hubs.fetch_hub!(identity_key)
20+
id = Keyword.fetch!(opts, :identity_key)
21+
team = Livebook.Hubs.fetch_hub!(id)
22+
23+
if :ets.whereis(__MODULE__) == :undefined do
24+
:ets.new(__MODULE__, [:named_table, :public, :set, read_concurrency: true])
25+
end
2026

2127
NimbleZTA.put(name, team)
2228
:ignore
2329
end
2430

25-
@impl true
31+
@impl NimbleZTA
2632
def authenticate(name, conn, _opts) do
2733
team = NimbleZTA.get(name)
2834

@@ -53,7 +59,10 @@ defmodule Livebook.ZTA.LivebookTeams do
5359

5460
defp handle_request(conn, team, %{"teams_identity" => _, "code" => code}) do
5561
with {:ok, access_token} <- retrieve_access_token(team, code),
56-
{:ok, metadata} <- get_user_info(team, access_token) do
62+
{:ok, payload} <- Teams.Requests.get_user_info(team, access_token) do
63+
metadata = build_metadata(team.id, payload)
64+
:ets.insert(__MODULE__, {access_token, {@exp_timestamp_sec, metadata}})
65+
5766
{conn
5867
|> put_session(:livebook_teams_access_token, access_token)
5968
|> redirect(to: conn.request_path)
@@ -99,7 +108,6 @@ defmodule Livebook.ZTA.LivebookTeams do
99108
%{"livebook_teams_access_token" => access_token} ->
100109
validate_access_token(conn, team, access_token)
101110

102-
# it means, we couldn't reach to Teams server
103111
%{"teams_error" => true} ->
104112
{conn
105113
|> put_status(:bad_request)
@@ -124,13 +132,6 @@ defmodule Livebook.ZTA.LivebookTeams do
124132
end
125133
end
126134

127-
defp validate_access_token(conn, team, access_token) do
128-
case get_user_info(team, access_token) do
129-
{:ok, metadata} -> {conn, metadata}
130-
_ -> request_user_authentication(conn)
131-
end
132-
end
133-
134135
defp retrieve_access_token(team, code) do
135136
with {:ok, %{"access_token" => access_token}} <-
136137
Teams.Requests.retrieve_access_token(team, code) do
@@ -167,9 +168,31 @@ defmodule Livebook.ZTA.LivebookTeams do
167168
{conn |> html(html_document) |> halt(), nil}
168169
end
169170

170-
defp get_user_info(team, access_token) do
171-
with {:ok, payload} <- Teams.Requests.get_user_info(team, access_token) do
172-
{:ok, build_metadata(team.id, payload)}
171+
defp validate_access_token(conn, team, access_token) do
172+
case Teams.Requests.get_user_info(team, access_token) do
173+
{:ok, payload} ->
174+
{conn, build_metadata(team.id, payload)}
175+
176+
:econnrefused ->
177+
data = :ets.lookup_element(__MODULE__, access_token, 2, nil)
178+
179+
case {System.os_time(:second), data} do
180+
{current_timestamp, {exp, metadata}} when current_timestamp <= exp ->
181+
{conn, metadata}
182+
183+
{_, entry} ->
184+
entry && :ets.delete(__MODULE__, access_token)
185+
186+
{conn
187+
|> put_status(:service_unavailable)
188+
|> put_view(LivebookWeb.ErrorHTML)
189+
|> render("503.html")
190+
|> halt(), nil}
191+
end
192+
193+
_otherwise ->
194+
:ets.delete(__MODULE__, access_token)
195+
request_user_authentication(conn)
173196
end
174197
end
175198

lib/livebook_web/controllers/error_html.ex

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ defmodule LivebookWeb.ErrorHTML do
2525
"""
2626
end
2727

28+
def render("503.html", assigns) do
29+
~H"""
30+
<.error_page
31+
status={503}
32+
title="Service unavailable"
33+
details="The server is currently down or under maintenance"
34+
/>
35+
"""
36+
end
37+
2838
def render(_template, assigns) do
2939
~H"""
3040
<.error_page
@@ -50,7 +60,13 @@ defmodule LivebookWeb.ErrorHTML do
5060
<link rel="icon" type="image/svg+xml" href={~p"/favicons/favicon.svg"} />
5161
<link rel="alternate icon" type="image/png" href={~p"/favicons/favicon.png"} />
5262
<title>{@status} - Livebook</title>
53-
<link rel="stylesheet" href={~p"/assets/app.css"} />
63+
<%= if LivebookWeb.Layouts.dev?() do %>
64+
<script phx-track-static type="module" src="http://localhost:4432/@vite/client">
65+
</script>
66+
<link phx-track-static rel="stylesheet" href="http://localhost:4432/css/app.css" />
67+
<% else %>
68+
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
69+
<% end %>
5470
</head>
5571
<body>
5672
<div class="h-screen flex items-center justify-center bg-gray-900">

test/livebook_teams/zta/livebook_teams_test.exs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,4 +113,52 @@ defmodule Livebook.ZTA.LivebookTeamsTest do
113113
assert html_response(conn, 200) =~ "window.location.href = "
114114
end
115115
end
116+
117+
defmodule Global do
118+
# We need to "turn off" the Teams API during test
119+
use Livebook.TeamsIntegrationCase, async: false
120+
121+
alias Livebook.ZTA.LivebookTeams
122+
123+
@moduletag teams_for: :agent
124+
setup :teams
125+
126+
@moduletag subscribe_to_hubs_topics: [:connection]
127+
@moduletag subscribe_to_teams_topics: [:clients, :agents]
128+
129+
test "uses cached version of the identity payload", %{test: test, team: team, node: node} do
130+
start_supervised!({LivebookTeams, name: test, identity_key: team.id})
131+
{conn, code} = authenticate_user_on_teams(test, node, team)
132+
133+
id = conn.assigns.current_user.id
134+
access_token = get_session(conn, :livebook_teams_access_token)
135+
groups = [%{"provider_id" => "1", "group_name" => "Foo"}]
136+
137+
# simulate the Teams API is down
138+
url = Livebook.Config.teams_url()
139+
Application.put_env(:livebook, :teams_url, "http://localhost:1234")
140+
141+
# update the groups, but doesn't return because Livebook is using the cached one
142+
TeamsRPC.update_user_info_groups(node, code, groups)
143+
assert {_, %{id: ^id, groups: []}} = LivebookTeams.authenticate(test, conn, [])
144+
145+
# simulate if the token already expired
146+
exp = System.os_time(:second) - 5 * 60
147+
{_, metadata} = :ets.lookup_element(LivebookTeams, access_token, 2)
148+
:ets.insert(LivebookTeams, {access_token, {exp, metadata}})
149+
150+
# now it should return status 503
151+
assert {%{status: 503, halted: true, resp_body: body}, nil} =
152+
LivebookTeams.authenticate(test, conn, [])
153+
154+
assert body =~ "The server is currently down or under maintenance"
155+
156+
# still show 503 error page because Teams isn't up yet
157+
assert {%{status: 503, halted: true}, nil} = LivebookTeams.authenticate(test, conn, [])
158+
159+
# now gets the updated userinfo from Teams
160+
Application.put_env(:livebook, :teams_url, url)
161+
assert {_conn, %{id: ^id, groups: ^groups}} = LivebookTeams.authenticate(test, conn, [])
162+
end
163+
end
116164
end

test/support/teams_integration_case.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ defmodule Livebook.TeamsIntegrationCase do
2727
url = TeamsServer.url()
2828
node = TeamsServer.get_node()
2929

30-
Application.put_env(:livebook, :teams_url, url, persistent: true)
30+
Application.put_env(:livebook, :teams_url, url)
3131

3232
{:ok, node: node}
3333
end

0 commit comments

Comments
 (0)