Skip to content

Commit e604e05

Browse files
committed
Add function to fetch the CLI session from Teams
1 parent 6806ef8 commit e604e05

8 files changed

Lines changed: 177 additions & 8 deletions

File tree

lib/livebook/hubs.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ defmodule Livebook.Hubs do
311311
@spec get_app_specs() :: list(Livebook.Apps.AppSpec.t())
312312
def get_app_specs() do
313313
for hub <- get_hubs(),
314+
Provider.connection_spec(hub),
314315
app_spec <- Provider.get_app_specs(hub),
315316
do: app_spec
316317
end

lib/livebook/hubs/team.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Team do
110110

111111
@teams_key_prefix Livebook.Teams.Org.teams_key_prefix()
112112
@public_key_prefix Team.public_key_prefix()
113+
@deploy_key_prefix Requests.deploy_key_prefix()
113114

114115
def load(team, fields) do
115116
{offline?, fields} = Map.pop(fields, :offline?, false)
@@ -137,6 +138,7 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Team do
137138

138139
def type(_team), do: "team"
139140

141+
def connection_spec(%{session_token: @deploy_key_prefix <> _}), do: nil
140142
def connection_spec(team), do: {TeamClient, team}
141143

142144
def disconnect(team), do: TeamClient.stop(team.id)

lib/livebook/migration.ex

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ defmodule Livebook.Migration do
1111
def run() do
1212
insert_personal_hub()
1313
remove_offline_hub()
14+
remove_cli_hub()
1415

1516
storage_version =
1617
case Storage.fetch_key(:system, "global", :migration_version) do
@@ -46,6 +47,19 @@ defmodule Livebook.Migration do
4647
end
4748
end
4849

50+
@deploy_key_prefix Livebook.Teams.Requests.deploy_key_prefix()
51+
52+
defp remove_cli_hub() do
53+
# The CLI hub will only be present in the storage if the
54+
# user doesn't have the Team hub already persisted with the
55+
# user credentials. Consequently, we always remove it and
56+
# insert on CLI if applicable.
57+
58+
for %{id: "team-" <> _ = id, session_token: @deploy_key_prefix <> _} <- Storage.all(:hubs) do
59+
:ok = Storage.delete(:hubs, id)
60+
end
61+
end
62+
4963
defp migration(1) do
5064
v1_add_personal_hub_secret_key()
5165
v1_delete_local_host_hub()

lib/livebook/teams.ex

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,36 @@ defmodule Livebook.Teams do
235235
TeamClient.get_environment_variables(team.id)
236236
end
237237

238+
@doc """
239+
Fetches the CLI session using a deploy key.
240+
"""
241+
@spec fetch_cli_session(map()) ::
242+
{:ok, Team.t()} | {:error, String.t()} | {:transport_error, String.t()}
243+
def fetch_cli_session(%{session_token: _, teams_key: _} = config) do
244+
with {:ok, %{"name" => name} = attrs} <- Requests.fetch_cli_session(config) do
245+
id = "team-#{name}"
246+
247+
hub =
248+
if Hubs.hub_exists?(id) do
249+
%{Hubs.fetch_hub!(id) | user_id: nil, session_token: config.session_token}
250+
else
251+
Hubs.save_hub(%Team{
252+
id: id,
253+
hub_name: name,
254+
hub_emoji: "🚀",
255+
user_id: nil,
256+
org_id: attrs["org_id"],
257+
org_key_id: attrs["org_key_id"],
258+
session_token: config.session_token,
259+
teams_key: config.teams_key,
260+
org_public_key: attrs["public_key"]
261+
})
262+
end
263+
264+
{:ok, hub}
265+
end
266+
end
267+
238268
defp map_teams_field_to_livebook_field(map, teams_field, livebook_field) do
239269
if value = map[teams_field] do
240270
Map.put_new(map, livebook_field, value)

lib/livebook/teams/requests.ex

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ defmodule Livebook.Teams.Requests do
55
alias Livebook.Secrets.Secret
66
alias Livebook.Teams
77

8+
@deploy_key_prefix "lb_dk_"
89
@error_message "Something went wrong, try again later or please file a bug if it persists"
910
@unauthorized_error_message "You are not authorized to perform this action, make sure you have the access and you are not in a Livebook App Server/Offline instance"
1011

@@ -14,6 +15,9 @@ defmodule Livebook.Teams.Requests do
1415
@doc false
1516
def error_message(), do: @error_message
1617

18+
@doc false
19+
def deploy_key_prefix(), do: @deploy_key_prefix
20+
1721
@doc """
1822
Send a request to Livebook Team API to create a new org.
1923
"""
@@ -227,6 +231,14 @@ defmodule Livebook.Teams.Requests do
227231
get("/api/v1/org/identity", %{access_token: access_token}, team)
228232
end
229233

234+
@doc """
235+
Send a request to Livebook Team API to return a session using a deploy key.
236+
"""
237+
@spec fetch_cli_session(map()) :: api_result()
238+
def fetch_cli_session(config) do
239+
post("/api/v1/cli/auth", %{}, config)
240+
end
241+
230242
@doc """
231243
Normalizes errors map into errors for the given schema.
232244
"""
@@ -291,6 +303,11 @@ defmodule Livebook.Teams.Requests do
291303
Req.Request.append_request_steps(req, unauthorized: &{&1, Req.Response.new(status: 401)})
292304
end
293305

306+
defp add_team_auth(req, %{session_token: @deploy_key_prefix <> _} = team) do
307+
token = "#{team.session_token}:#{Teams.Org.key_hash(%Teams.Org{teams_key: team.teams_key})}"
308+
Req.Request.merge_options(req, auth: {:bearer, token})
309+
end
310+
294311
defp add_team_auth(req, %{user_id: nil} = team) do
295312
agent_name = Livebook.Config.agent_name()
296313
token = "#{team.session_token}:#{agent_name}:#{team.org_id}:#{team.org_key_id}"

test/livebook_teams/teams_test.exs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,4 +250,51 @@ defmodule Livebook.TeamsTest do
250250
assert_receive {:app_deployment_stopped, ^app_deployment2}
251251
end
252252
end
253+
254+
describe "fetch_cli_session/1" do
255+
@describetag teams_for: :cli
256+
257+
@tag teams_persisted: false
258+
test "authenticates the deploy key", %{team: team} do
259+
config = %{teams_key: team.teams_key, session_token: team.session_token}
260+
261+
assert Teams.fetch_cli_session(config) == {:ok, team}
262+
assert Livebook.Hubs.hub_exists?(team.id)
263+
end
264+
265+
@tag teams_for: :user
266+
test "authenticates the deploy key when hub already exists",
267+
%{team: team, org: org, node: node} do
268+
{key, _} = TeamsRPC.create_deploy_key(node, org: org)
269+
config = %{teams_key: team.teams_key, session_token: key}
270+
271+
assert Teams.fetch_cli_session(config) ==
272+
{:ok,
273+
%Livebook.Hubs.Team{
274+
billing_status: team.billing_status,
275+
hub_emoji: team.hub_emoji,
276+
hub_name: team.hub_name,
277+
id: team.id,
278+
offline: nil,
279+
org_id: team.org_id,
280+
org_key_id: team.org_key_id,
281+
org_public_key: team.org_public_key,
282+
session_token: key,
283+
teams_key: team.teams_key,
284+
user_id: nil
285+
}}
286+
287+
# If the hub already exist, we don't update them from storage
288+
assert Livebook.Hubs.hub_exists?(team.id)
289+
refute Livebook.Hubs.fetch_hub!(team.id).session_token == key
290+
end
291+
292+
@tag teams_persisted: false
293+
test "returns error with invalid credentials", %{team: team} do
294+
config = %{teams_key: team.teams_key, session_token: "lb_dk_foo"}
295+
296+
assert {:transport_error, "You are not authorized" <> _} = Teams.fetch_cli_session(config)
297+
refute Livebook.Hubs.hub_exists?(team.id)
298+
end
299+
end
253300
end

test/support/integration/teams_rpc.ex

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,11 @@ defmodule Livebook.TeamsRPC do
137137
:erpc.call(node, TeamsRPC, :create_authorization_group, [attrs])
138138
end
139139

140+
def create_deploy_key(node, attrs \\ []) do
141+
key = :erpc.call(node, TeamsRPC, :generate_deploy_key, [])
142+
{key, :erpc.call(node, TeamsRPC, :create_deploy_key, [key, attrs])}
143+
end
144+
140145
# Update resource
141146

142147
def update_authorization_group(node, authorization_group, attrs) do

test/support/integration/teams_tests.ex

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ defmodule Livebook.TeamsIntegrationHelper do
1212
{:user, _} -> Map.merge(context, create_user_hub(context.node))
1313
{:agent, false} -> Map.merge(context, new_agent_hub(context.node))
1414
{:agent, _} -> Map.merge(context, create_agent_hub(context.node))
15+
{:cli, false} -> Map.merge(context, new_cli_hub(context.node))
16+
{:cli, _} -> Map.merge(context, create_cli_hub(context.node))
1517
_otherwise -> context
1618
end
1719
end
1820

19-
def livebook_teams_auth(%{conn: conn, node: node, team: team} = context) do
21+
def livebook_teams_auth(%{node: node, team: team} = context) do
2022
ZTA.LivebookTeams.start_link(name: context.test, identity_key: team.id)
21-
{conn, code} = authenticate_user_on_teams(context.test, conn, node, team)
23+
{conn, code} = authenticate_user_on_teams(context.test, node, team)
2224

2325
Map.merge(context, %{conn: conn, code: code})
2426
end
@@ -123,12 +125,64 @@ defmodule Livebook.TeamsIntegrationHelper do
123125
}
124126
end
125127

126-
defp authenticate_user_on_teams(name, conn, node, team) do
127-
# Create a fresh connection to avoid session contamination
128-
fresh_conn = Phoenix.ConnTest.build_conn()
128+
def create_cli_hub(node, opts \\ []) do
129+
context = new_cli_hub(node, opts)
130+
131+
Hubs.save_hub(context.team)
132+
ExUnit.Callbacks.on_exit(fn -> Hubs.delete_hub(context.team.id) end)
133+
134+
%{context | team: Hubs.fetch_hub!(context.team.id)}
135+
end
136+
137+
def new_cli_hub(node, opts \\ []) do
138+
{teams_key, key_hash} = generate_key_hash()
139+
140+
org = TeamsRPC.create_org(node)
141+
org_key = TeamsRPC.create_org_key(node, org: org, key_hash: key_hash)
142+
org_key_pair = TeamsRPC.create_org_key_pair(node, org: org)
143+
144+
attrs =
145+
opts
146+
|> Keyword.get(:deployment_group, [])
147+
|> Keyword.merge(
148+
name: "angry-cat-#{Ecto.UUID.generate()}",
149+
mode: :online,
150+
org: org
151+
)
152+
153+
deployment_group = TeamsRPC.create_deployment_group(node, attrs)
154+
{key, deploy_key} = TeamsRPC.create_deploy_key(node, org: org)
155+
156+
TeamsRPC.create_billing_subscription(node, org)
157+
158+
team =
159+
Factory.build(:team,
160+
id: "team-#{org.name}",
161+
hub_name: org.name,
162+
hub_emoji: "🚀",
163+
user_id: nil,
164+
org_id: org.id,
165+
org_key_id: org_key.id,
166+
org_public_key: org_key_pair.public_key,
167+
session_token: key,
168+
teams_key: teams_key
169+
)
170+
171+
%{
172+
deploy_key: Map.replace!(deploy_key, :key_hash, key),
173+
deployment_group: deployment_group,
174+
org: org,
175+
org_key: org_key,
176+
org_key_pair: org_key_pair,
177+
team: team
178+
}
179+
end
180+
181+
def authenticate_user_on_teams(name, node, team) do
182+
conn = Phoenix.ConnTest.build_conn()
129183

130184
response =
131-
fresh_conn
185+
conn
132186
|> LivebookWeb.ConnCase.with_authorization(team.id, name)
133187
|> get("/")
134188
|> html_response(200)
@@ -140,12 +194,11 @@ defmodule Livebook.TeamsIntegrationHelper do
140194
%{code: code} = Livebook.TeamsRPC.allow_auth_request(node, token)
141195

142196
session =
143-
fresh_conn
197+
conn
144198
|> LivebookWeb.ConnCase.with_authorization(team.id, name)
145199
|> get("/", %{teams_identity: "", code: code})
146200
|> Plug.Conn.get_session()
147201

148-
# Initialize the original conn with the new session data
149202
authenticated_conn = Plug.Test.init_test_session(conn, session)
150203
final_conn = get(authenticated_conn, "/")
151204
assigns = Map.take(final_conn.assigns, [:current_user])

0 commit comments

Comments
 (0)