Allow hub secret toggling (#1675)

This commit is contained in:
Alexandre de Souza 2023-02-03 13:15:46 -03:00 committed by GitHub
parent 605498a369
commit 5f6e7176fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 515 additions and 252 deletions

View file

@ -82,7 +82,7 @@ defmodule Livebook.Hubs do
attributes = struct |> Map.from_struct() |> Map.to_list()
:ok = Storage.insert(@namespace, struct.id, attributes)
:ok = connect_hub(struct)
:ok = Broadcasts.hubs_metadata_changed()
:ok = Broadcasts.hub_changed()
struct
end
@ -90,14 +90,23 @@ defmodule Livebook.Hubs do
@doc false
def delete_hub(id) do
with {:ok, hub} <- get_hub(id) do
:ok = Provider.disconnect(hub)
:ok = Broadcasts.hub_changed()
:ok = Storage.delete(@namespace, id)
:ok = Broadcasts.hubs_metadata_changed()
:ok = disconnect_hub(hub)
end
:ok
end
defp disconnect_hub(hub) do
Task.Supervisor.start_child(Livebook.TaskSupervisor, fn ->
Process.sleep(30_000)
:ok = Provider.disconnect(hub)
end)
:ok
end
@doc false
def clean_hubs do
for hub <- get_hubs(), do: delete_hub(hub.id)
@ -112,7 +121,7 @@ defmodule Livebook.Hubs do
Topic `hubs:crud`:
* `:hubs_metadata_changed`
* `:hub_changed`
Topic `hubs:connection`:

View file

@ -12,9 +12,9 @@ defmodule Livebook.Hubs.Broadcasts do
@doc """
Broadcasts when hubs changed under `hubs:crud` topic
"""
@spec hubs_metadata_changed() :: broadcast()
def hubs_metadata_changed do
broadcast(@crud_topic, :hubs_metadata_changed)
@spec hub_changed() :: broadcast()
def hub_changed do
broadcast(@crud_topic, :hub_changed)
end
@doc """

View file

@ -24,16 +24,16 @@ defmodule LivebookWeb.SidebarHook do
{:halt, put_flash(socket, :info, "Livebook is shutting down. You can close this page.")}
end
@connection_events ~w(hub_connected hub_disconnected hubs_metadata_changed)a
@connection_events ~w(hub_connected hub_disconnected hubs_changed)a
defp handle_info(event, socket) when event in @connection_events do
{:halt, assign(socket, saved_hubs: Livebook.Hubs.get_metadatas())}
{:cont, assign(socket, saved_hubs: Livebook.Hubs.get_metadatas())}
end
@error_events ~w(hub_connection_failed hub_disconnection_failed)a
defp handle_info({event, _reason}, socket) when event in @error_events do
{:halt, assign(socket, saved_hubs: Livebook.Hubs.get_metadatas())}
{:cont, assign(socket, saved_hubs: Livebook.Hubs.get_metadatas())}
end
defp handle_info(_event, socket), do: {:cont, socket}

View file

@ -277,12 +277,14 @@ defmodule LivebookWeb.LiveHelpers do
<.switch_checkbox
name="likes_cats"
label="I very much like cats"
tooltip="Cats"
checked={@likes_cats} />
"""
def switch_checkbox(assigns) do
assigns =
assigns
|> assign_new(:label, fn -> nil end)
|> assign_new(:tooltip, fn -> nil end)
|> assign_new(:disabled, fn -> false end)
|> assign_new(:class, fn -> "" end)
|> assign(
@ -293,7 +295,7 @@ defmodule LivebookWeb.LiveHelpers do
~H"""
<div class="flex items-center gap-1 sm:gap-3 justify-between">
<%= if @label do %>
<span class="text-gray-700"><%= @label %></span>
<span class="text-gray-700 tooltip top" data-tooltip={@tooltip}><%= @label %></span>
<% end %>
<label class={"switch-button #{if(@disabled, do: "switch-button--disabled")}"}>
<input type="hidden" value="false" name={@name} />

View file

@ -183,6 +183,7 @@ defmodule LivebookWeb.SessionLive do
<.secrets_list
data_view={@data_view}
saved_secrets={@saved_secrets}
hubs={@saved_hubs}
session={@session}
socket={@socket}
/>
@ -657,7 +658,12 @@ defmodule LivebookWeb.SessionLive do
<div class="flex flex-col space-y-4 mt-6">
<%= for secret when secret.origin in [:app, :startup] <- @saved_secrets do %>
<.secrets_item secret={secret} prefix="app" data_secrets={@data_view.secrets} />
<.secrets_item
secret={secret}
prefix={to_string(secret.origin)}
data_secrets={@data_view.secrets}
hubs={@hubs}
/>
<% end %>
</div>
@ -677,7 +683,12 @@ defmodule LivebookWeb.SessionLive do
<div class="flex flex-col space-y-4 mt-6">
<%= for %{origin: {:hub, id}} = secret <- @saved_secrets do %>
<.secrets_item secret={secret} prefix={"hub-#{id}"} data_secrets={@data_view.secrets} />
<.secrets_item
secret={secret}
prefix={"hub-#{id}"}
data_secrets={@data_view.secrets}
hubs={@hubs}
/>
<% end %>
</div>
<% end %>
@ -705,15 +716,15 @@ defmodule LivebookWeb.SessionLive do
>
<%= @secret.name %>
</span>
<%= if @secret.origin in [:app, :startup] do %>
<.switch_checkbox
name="toggle_secret"
checked={is_secret_on_session?(@secret, @data_secrets)}
phx-click="toggle_secret"
phx-value-secret_name={@secret.name}
phx-value-secret_value={@secret.value}
/>
<% end %>
<.switch_checkbox
name="toggle_secret"
checked={secret_toggled?(@secret, @data_secrets)}
label={secret_label(@secret, @hubs)}
tooltip={secret_tooltip(@secret, @hubs)}
phx-click="toggle_secret"
phx-value-secret_name={@secret.name}
phx-value-secret_value={@secret.value}
/>
</div>
<div class="flex flex-col text-gray-800 hidden" id={"#{@prefix}-secret-#{@secret.name}-detail"}>
<div class="flex flex-col">
@ -730,15 +741,15 @@ defmodule LivebookWeb.SessionLive do
>
<%= @secret.name %>
</span>
<%= if @secret.origin in [:app, :startup] do %>
<.switch_checkbox
name="toggle_secret"
checked={is_secret_on_session?(@secret, @data_secrets)}
phx-click="toggle_secret"
phx-value-secret_name={@secret.name}
phx-value-secret_value={@secret.value}
/>
<% end %>
<.switch_checkbox
name="toggle_secret"
checked={secret_toggled?(@secret, @data_secrets)}
label={secret_label(@secret, @hubs)}
tooltip={secret_tooltip(@secret, @hubs)}
phx-click="toggle_secret"
phx-value-secret_name={@secret.name}
phx-value-secret_value={@secret.value}
/>
</div>
<div class="flex flex-row justify-between items-center my-1">
<span class="text-sm font-mono break-all flex-row">
@ -1436,6 +1447,10 @@ defmodule LivebookWeb.SessionLive do
|> put_flash(:info, "An existing secret has been updated on your Livebook Enterprise")}
end
def handle_info(:hubs_changed, socket) do
{:noreply, assign(socket, saved_secrets: get_saved_secrets())}
end
def handle_info({:error, error}, socket) do
message = error |> to_string() |> upcase_first()
@ -2273,11 +2288,21 @@ defmodule LivebookWeb.SessionLive do
:ok
end
defp is_secret_on_session?(secret, secrets) do
Map.has_key?(secrets, secret.name)
defp secret_toggled?(secret, secrets) do
Map.has_key?(secrets, secret.name) and secrets[secret.name] == secret.value
end
defp get_saved_secrets do
Enum.sort(Hubs.get_secrets() ++ Secrets.get_secrets())
end
defp secret_label(%{origin: {:hub, id}}, hubs), do: fetch_hub!(id, hubs).emoji
defp secret_label(_, _), do: nil
defp secret_tooltip(%{origin: {:hub, id}}, hubs), do: fetch_hub!(id, hubs).name
defp secret_tooltip(_, _), do: nil
defp fetch_hub!(id, hubs) do
Enum.find(hubs, &(&1.id == id)) || raise "unknown hub id: #{id}"
end
end

View file

@ -10,22 +10,7 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
|> assign(assigns)
|> assign(hubs: Livebook.Hubs.get_hubs([:secrets]))
prefill_form = prefill_secret_name(socket)
socket =
if socket.assigns[:data] do
socket
else
assign(socket,
data: %{"name" => prefill_form, "value" => "", "store" => "session"},
errors: [{"value", {"can't be blank", []}}],
title: title(socket),
grant_access: must_grant_access(socket),
has_prefill: prefill_form != ""
)
end
{:ok, socket}
{:ok, assign(socket, prefill_assigns(socket))}
end
@impl true
@ -35,8 +20,12 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
<h3 class="text-2xl font-semibold text-gray-800">
<%= @title %>
</h3>
<%= if @grant_access do %>
<.grant_access_message grant_access={@grant_access} target={@myself} />
<%= if @grant_access_name do %>
<.grant_access_message
secret_name={@grant_access_name}
secret_origin={@grant_access_origin}
target={@myself}
/>
<% end %>
<div class="flex flex-columns gap-4">
<%= if @select_secret_ref do %>
@ -49,7 +38,7 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
<%= for {secret_name, _} <- Enum.sort(@secrets) do %>
<.secret_with_badge
secret_name={secret_name}
origin="session"
secret_origin="session"
stored="Session"
action="select_secret"
active={secret_name == @prefill_secret_name}
@ -59,7 +48,8 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
<%= for secret <- @saved_secrets do %>
<.secret_with_badge
secret_name={secret.name}
origin={origin(secret)}
secret_store={store(secret)}
secret_origin={origin(secret)}
stored={stored(secret)}
action="select_secret"
active={false}
@ -150,6 +140,44 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
"""
end
defp secret_with_badge(%{secret_store: "hub"} = assigns) do
~H"""
<div
role="button"
class={[
"flex justify-between w-full font-mono text-sm p-2 border-b cursor-pointer",
if @active do
"bg-blue-100 text-blue-700"
else
"text-gray-700 hover:bg-gray-100"
end
]}
phx-value-name={@secret_name}
phx-value-store="hub"
phx-value-hub_id={@secret_origin}
phx-target={@target}
phx-click={@action}
>
<%= @secret_name %>
<span class={[
"inline-flex items-center font-sans rounded-full px-2.5 py-0.5 text-xs font-medium bg-gray-100",
if @active do
"bg-indigo-100 text-blue-800"
else
"bg-gray-100 text-gray-800"
end
]}>
<%= if @active do %>
<svg class="-ml-0.5 mr-1.5 h-2 w-2 text-blue-400" fill="currentColor" viewBox="0 0 8 8">
<circle cx="4" cy="4" r="3" />
</svg>
<% end %>
<%= @stored %>
</span>
</div>
"""
end
defp secret_with_badge(assigns) do
~H"""
<div
@ -162,8 +190,8 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
"text-gray-700 hover:bg-gray-100"
end
]}
phx-value-secret_name={@secret_name}
phx-value-origin={@origin}
phx-value-name={@secret_name}
phx-value-store={@secret_store}
phx-target={@target}
phx-click={@action}
>
@ -198,20 +226,42 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
icon="error-warning-fill"
class="align-middle text-2xl flex text-gray-100 rounded-lg py-2"
/>
<span class="ml-2 text-sm font-normal text-gray-100">
There is a secret named
<span class="font-semibold text-white"><%= @grant_access %></span>
in your Livebook app. Allow this session to access it?
</span>
<%= if @secret_origin in ["app", "startup"] do %>
<span class="ml-2 text-sm font-normal text-gray-100">
There is a secret named
<span class="font-semibold text-white"><%= @secret_name %></span>
in your Livebook app. Allow this session to access it?
</span>
<% else %>
<span class="ml-2 text-sm font-normal text-gray-100">
There is a secret named
<span class="font-semibold text-white"><%= @secret_name %></span>
in your Livebook Hub. Allow this session to access it?
</span>
<% end %>
</div>
<button
class="button-base button-gray"
phx-click="grant_access"
phx-value-secret_name={@grant_access}
phx-target={@target}
>
Grant access
</button>
<%= if @secret_origin in ["app", "startup"] do %>
<button
class="button-base button-gray"
phx-click="grant_access"
phx-value-name={@secret_name}
phx-value-store={@secret_origin}
phx-target={@target}
>
Grant access
</button>
<% else %>
<button
class="button-base button-gray"
phx-click="grant_access"
phx-value-name={@secret_name}
phx-value-store="hub"
phx-value-hub_id={@secret_origin}
phx-target={@target}
>
Grant access
</button>
<% end %>
</div>
</div>
</div>
@ -219,6 +269,33 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
"""
end
defp prefill_assigns(socket) do
secret_name = socket.assigns[:prefill_secret_name]
assigns = %{
data: %{"name" => secret_name, "value" => "", "store" => "session"},
errors: [{"value", {"can't be blank", []}}],
title: title(socket),
grant_access_name: nil,
grant_access_origin: "app",
has_prefill: !is_nil(secret_name)
}
case Enum.find(socket.assigns.saved_secrets, &(&1.name == secret_name)) do
%Secret{name: name, origin: {:hub, id}} ->
%{assigns | grant_access_name: name, grant_access_origin: id}
%Secret{name: name, origin: origin} ->
%{assigns | grant_access_name: name, grant_access_origin: to_string(origin)}
nil ->
assigns
end
end
defp store(%{origin: {:hub, _id}}), do: "hub"
defp store(%{origin: origin}), do: to_string(origin)
defp origin(%{origin: {:hub, id}}), do: id
defp origin(%{origin: origin}), do: to_string(origin)
@ -237,34 +314,11 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
else
{:error, %{errors: errors}} ->
{:noreply, assign(socket, errors: errors)}
{:error, socket} ->
{:noreply, socket}
end
end
def handle_event(
"select_secret",
%{"secret_name" => secret_name, "origin" => "session"},
socket
) do
{:noreply,
socket
|> push_patch(to: socket.assigns.return_to)
|> push_secret_selected(secret_name)}
end
def handle_event("select_secret", %{"secret_name" => secret_name, "origin" => "app"}, socket) do
grant_access(socket.assigns.saved_secrets, secret_name, :app, socket)
{:noreply,
socket
|> push_patch(to: socket.assigns.return_to)
|> push_secret_selected(secret_name)}
end
def handle_event("select_secret", %{"secret_name" => secret_name, "origin" => hub_id}, socket) do
grant_access(socket.assigns.saved_secrets, secret_name, {:hub, hub_id}, socket)
def handle_event("select_secret", %{"name" => secret_name} = attrs, socket) do
grant_access(socket.assigns.saved_secrets, secret_name, build_origin(attrs), socket)
{:noreply,
socket
@ -281,8 +335,8 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
end
end
def handle_event("grant_access", %{"secret_name" => secret_name}, socket) do
grant_access(socket.assigns.saved_secrets, secret_name, :app, socket)
def handle_event("grant_access", %{"name" => secret_name} = attrs, socket) do
grant_access(socket.assigns.saved_secrets, secret_name, build_origin(attrs), socket)
{:noreply,
socket
@ -296,28 +350,12 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
push_event(socket, "secret_selected", %{select_secret_ref: ref, secret_name: secret_name})
end
defp prefill_secret_name(socket) do
if unavailable_secret?(socket, socket.assigns.prefill_secret_name),
do: socket.assigns.prefill_secret_name,
else: ""
end
defp unavailable_secret?(_socket, nil), do: false
defp unavailable_secret?(_socket, ""), do: false
defp unavailable_secret?(socket, preselect_name) do
not session?(socket, preselect_name) and
not app?(socket, preselect_name) and
not hub?(socket, preselect_name)
end
defp title(%{assigns: %{select_secret_ref: nil}}), do: "Add secret"
defp title(%{assigns: %{select_secret_options: %{"title" => title}}}), do: title
defp title(_), do: "Select secret"
defp build_origin(%{"store" => "session"}), do: :session
defp build_origin(%{"store" => "app"}), do: :app
defp build_origin(%{"store" => "hub", "hub_id" => id}), do: {:hub, id}
defp build_origin(%{"store" => store}), do: String.to_existing_atom(store)
defp build_attrs(%{"name" => name, "value" => value} = attrs) do
%{name: name, value: value, origin: build_origin(attrs)}
@ -332,38 +370,16 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
Livebook.Session.set_secret(socket.assigns.session.pid, secret)
end
defp set_secret(_socket, %Secret{origin: {:hub, id}} = secret) when is_binary(id) do
Livebook.Hubs.create_secret(secret)
defp set_secret(socket, %Secret{origin: {:hub, id}} = secret) when is_binary(id) do
with :ok <- Livebook.Hubs.create_secret(secret) do
Livebook.Session.set_secret(socket.assigns.session.pid, secret)
end
end
defp grant_access(secrets, secret_name, origin, socket) do
secret = Enum.find(secrets, &(&1.name == secret_name and &1.origin == origin))
if secret,
do: set_secret(socket, secret),
else: :ok
end
defp must_grant_access(%{assigns: %{prefill_secret_name: secret_name}} = socket) do
if not session?(socket, secret_name) and
(app?(socket, secret_name) or hub?(socket, secret_name)) do
secret_name
end
end
defp session?(socket, secret_name) do
Enum.any?(socket.assigns.secrets, &(elem(&1, 0) == secret_name))
end
defp app?(socket, secret_name) do
Enum.any?(
socket.assigns.saved_secrets,
&(&1.name == secret_name and &1.origin in [:app, :startup])
)
end
defp hub?(socket, secret_name) do
Enum.any?(socket.assigns.saved_secrets, &(&1.name == secret_name))
if secret, do: Livebook.Session.set_secret(socket.assigns.session.pid, secret)
end
defp hubs_options(hubs, hub_id) do

View file

@ -1,14 +1,21 @@
defmodule LivebookWeb.SessionLive.SecretsComponentTest do
use Livebook.EnterpriseIntegrationCase, async: true
import Livebook.SessionHelpers
import Phoenix.LiveViewTest
alias Livebook.Session
alias Livebook.Sessions
describe "enterprise" do
setup %{url: url, token: token} do
id = Livebook.Utils.random_short_id()
setup %{test: name} do
start_new_instance(name)
node = EnterpriseServer.get_node(name)
url = EnterpriseServer.url(name)
token = EnterpriseServer.token(name)
id = :erpc.call(node, Enterprise.Integration, :fetch_env!, ["ENTERPRISE_ID"])
hub_id = "enterprise-#{id}"
Livebook.Hubs.subscribe([:connection, :secrets])
@ -25,38 +32,18 @@ defmodule LivebookWeb.SessionLive.SecretsComponentTest do
{:ok, session} = Sessions.create_session(notebook: Livebook.Notebook.new())
on_exit(fn ->
Livebook.Hubs.delete_hub(hub_id)
Session.close(session.pid)
stop_new_instance(name)
end)
{:ok, enterprise: enterprise, session: session}
{:ok, enterprise: enterprise, session: session, node: node}
end
test "shows the connected hubs dropdown", %{
conn: conn,
session: session,
enterprise: enterprise
} do
secret = build(:secret, name: "LESS_IMPORTANT_SECRET", value: "123", origin: enterprise.id)
{:ok, view, _html} = live(conn, Routes.session_path(conn, :secrets, session.id))
assert view
|> element(~s{form[phx-submit="save"]})
|> render_change(%{
data: %{
name: secret.name,
value: secret.value,
store: "hub"
}
}) =~ ~s(<option value="#{enterprise.id}">#{enterprise.hub_name}</option>)
end
test "creates a secret on Enterprise hub", %{
conn: conn,
session: session,
enterprise: enterprise
} do
test "creates a secret on Enterprise hub",
%{conn: conn, session: session, enterprise: enterprise} do
id = enterprise.id
secret = build(:secret, name: "BIG_IMPORTANT_SECRET", value: "123", origin: id)
secret = build(:secret, name: "BIG_IMPORTANT_SECRET", value: "123", origin: {:hub, id})
{:ok, view, _html} = live(conn, Routes.session_path(conn, :secrets, session.id))
attrs = %{
@ -72,8 +59,142 @@ defmodule LivebookWeb.SessionLive.SecretsComponentTest do
render_change(form, attrs)
render_submit(form, attrs)
assert_receive {:secret_created, ^secret}
assert render(view) =~ "A new secret has been created on your Livebook Enterprise"
assert has_element?(view, "#hub-#{enterprise.id}-secret-#{attrs.data.name}-title")
assert has_element?(
view,
"#hub-#{enterprise.id}-secret-#{secret.name}-title span",
enterprise.hub_emoji
)
end
test "toggle a secret from Enterprise hub",
%{conn: conn, session: session, enterprise: enterprise, node: node} do
secret =
build(:secret,
name: "POSTGRES_PASSWORD",
value: "postgres",
origin: {:hub, enterprise.id}
)
{:ok, view, _html} = live(conn, Routes.session_path(conn, :page, session.id))
:erpc.call(node, Enterprise.Integration, :create_secret, [secret.name, secret.value])
assert_receive {:secret_created, ^secret}
Session.set_secret(session.pid, secret)
assert_session_secret(view, session.pid, secret)
end
test "adding a missing secret using 'Add secret' button",
%{conn: conn, session: session, enterprise: enterprise} do
secret =
build(:secret,
name: "PGPASS",
value: "postgres",
origin: {:hub, enterprise.id}
)
# Subscribe and executes the code to trigger
# the `System.EnvError` exception and outputs the 'Add secret' button
Session.subscribe(session.id)
section_id = insert_section(session.pid)
code = ~s{System.fetch_env!("LB_#{secret.name}")}
cell_id = insert_text_cell(session.pid, section_id, :code, code)
Session.queue_cell_evaluation(session.pid, cell_id)
assert_receive {:operation, {:add_cell_evaluation_response, _, ^cell_id, _, _}}
# Enters the session to check if the button exists
{:ok, view, _} = live(conn, "/sessions/#{session.id}")
expected_url = Routes.session_path(conn, :secrets, session.id, secret_name: secret.name)
add_secret_button = element(view, "a[href='#{expected_url}']")
assert has_element?(add_secret_button)
# Clicks the button and fills the form to create a new secret
# that prefilled the name with the received from exception.
render_click(add_secret_button)
secrets_component = with_target(view, "#secrets-modal")
form_element = element(secrets_component, "form[phx-submit='save']")
assert has_element?(form_element)
data = %{value: secret.value, store: "hub", hub_id: enterprise.id}
render_submit(form_element, %{data: data})
# Checks we received the secret created event from Enterprise
assert_receive {:secret_created, ^secret}
# Checks if the secret is persisted
assert secret in Livebook.Hubs.get_secrets()
# Checks if the secret exists and is inside the session,
# then executes the code cell again and checks if the
# secret value is what we expected.
assert_session_secret(view, session.pid, secret)
Session.queue_cell_evaluation(session.pid, cell_id)
assert_receive {:operation,
{:add_cell_evaluation_response, _, ^cell_id, {:text, output}, _}}
assert output == "\e[32m\"#{secret.value}\"\e[0m"
end
test "granting access for missing secret using 'Add secret' button",
%{conn: conn, session: session, enterprise: enterprise, node: node} do
secret =
build(:secret,
name: "MYSQL_PASS",
value: "admin",
origin: {:hub, enterprise.id}
)
# Subscribe and executes the code to trigger
# the `System.EnvError` exception and outputs the 'Add secret' button
Session.subscribe(session.id)
section_id = insert_section(session.pid)
code = ~s{System.fetch_env!("LB_#{secret.name}")}
cell_id = insert_text_cell(session.pid, section_id, :code, code)
Session.queue_cell_evaluation(session.pid, cell_id)
assert_receive {:operation, {:add_cell_evaluation_response, _, ^cell_id, _, _}}
# Enters the session to check if the button exists
{:ok, view, _} = live(conn, "/sessions/#{session.id}")
expected_url = Routes.session_path(conn, :secrets, session.id, secret_name: secret.name)
add_secret_button = element(view, "a[href='#{expected_url}']")
assert has_element?(add_secret_button)
# Persist the secret from the Enterprise
:erpc.call(node, Enterprise.Integration, :create_secret, [secret.name, secret.value])
# Grant we receive the event, even with eventually delay
assert_receive {:secret_created, ^secret}, 10_000
# Checks if the secret is persisted
assert secret in Livebook.Hubs.get_secrets()
# Clicks the button and checks if the 'Grant access' banner
# is being shown, so clicks it's button to set the app secret
# to the session, allowing the user to fetches the secret.
render_click(add_secret_button)
secrets_component = with_target(view, "#secrets-modal")
assert render(secrets_component) =~ "in your Livebook Hub. Allow this session to access it?"
grant_access_button = element(secrets_component, "button", "Grant access")
render_click(grant_access_button)
# Checks if the secret exists and is inside the session,
# then executes the code cell again and checks if the
# secret value is what we expected.
assert_session_secret(view, session.pid, secret)
Session.queue_cell_evaluation(session.pid, cell_id)
assert_receive {:operation,
{:add_cell_evaluation_response, _, ^cell_id, {:text, output}, _}}
assert output == "\e[32m\"#{secret.value}\"\e[0m"
end
end
end

View file

@ -1,6 +1,7 @@
defmodule LivebookWeb.SessionLiveTest do
use LivebookWeb.ConnCase, async: true
import Livebook.SessionHelpers
import Phoenix.LiveViewTest
alias Livebook.{Sessions, Session, Settings, Runtime, Users, FileSystem}
@ -921,7 +922,7 @@ defmodule LivebookWeb.SessionLiveTest do
|> Plug.Conn.resp(200, "# My notebook")
end)
index_url = url(bypass.port) <> "/index.livemd"
index_url = bypass_url(bypass.port) <> "/index.livemd"
{:ok, session} = Sessions.create_session(origin: {:url, index_url})
assert {:error, {:live_redirect, %{to: "/sessions/" <> session_id}}} =
@ -949,7 +950,7 @@ defmodule LivebookWeb.SessionLiveTest do
|> Plug.Conn.resp(200, "# My notebook")
end)
index_url = url(bypass.port) <> "/index.livemd"
index_url = bypass_url(bypass.port) <> "/index.livemd"
{:ok, session} = Sessions.create_session(origin: {:url, index_url})
assert {:error, {:live_redirect, %{to: "/sessions/" <> session_id}}} =
@ -969,7 +970,7 @@ defmodule LivebookWeb.SessionLiveTest do
Plug.Conn.resp(conn, 500, "Error")
end)
index_url = url(bypass.port) <> "/index.livemd"
index_url = bypass_url(bypass.port) <> "/index.livemd"
{:ok, session} = Sessions.create_session(origin: {:url, index_url})
@ -1108,7 +1109,7 @@ defmodule LivebookWeb.SessionLiveTest do
assert app_secret in Livebook.Secrets.get_secrets()
end
test "shows the 'Add secret' button for unavailable secrets", %{conn: conn, session: session} do
test "shows the 'Add secret' button for missing secrets", %{conn: conn, session: session} do
secret = build(:secret, name: "ANOTHER_GREAT_SECRET", value: "123456", origin: :session)
Session.subscribe(session.id)
section_id = insert_section(session.pid)
@ -1125,9 +1126,12 @@ defmodule LivebookWeb.SessionLiveTest do
|> has_element?()
end
test "adding an unavailable secret using 'Add secret' button",
test "adding a missing secret using 'Add secret' button",
%{conn: conn, session: session} do
secret = build(:secret, name: "MYUNAVAILABLESECRET", value: "123456", origin: :session)
# Subscribe and executes the code to trigger
# the `System.EnvError` exception and outputs the 'Add secret' button
Session.subscribe(session.id)
section_id = insert_section(session.pid)
code = ~s{System.fetch_env!("LB_#{secret.name}")}
@ -1136,24 +1140,27 @@ defmodule LivebookWeb.SessionLiveTest do
Session.queue_cell_evaluation(session.pid, cell_id)
assert_receive {:operation, {:add_cell_evaluation_response, _, ^cell_id, _, _}}
# Enters the session to check if the button exists
{:ok, view, _} = live(conn, "/sessions/#{session.id}")
expected_url = Routes.session_path(conn, :secrets, session.id, secret_name: secret.name)
add_secret_button = element(view, "a[href='#{expected_url}']")
assert has_element?(add_secret_button)
render_click(add_secret_button)
# Clicks the button and fills the form to create a new secret
# that prefilled the name with the received from exception.
render_click(add_secret_button)
secrets_component = with_target(view, "#secrets-modal")
form_element = element(secrets_component, "form[phx-submit='save']")
assert has_element?(form_element)
render_submit(form_element, %{data: %{value: secret.value, store: "session"}})
assert_session_secret(view, session.pid, secret)
# Checks if the secret isn't an app secret
refute secret in Livebook.Secrets.get_secrets()
# Checks if the secret exists and is inside the session,
# then executes the code cell again and checks if the
# secret value is what we expected.
assert_session_secret(view, session.pid, secret)
Session.queue_cell_evaluation(session.pid, cell_id)
assert_receive {:operation,
@ -1165,6 +1172,9 @@ defmodule LivebookWeb.SessionLiveTest do
test "granting access for unavailable secret using 'Add secret' button",
%{conn: conn, session: session} do
secret = insert_secret(name: "UNAVAILABLESECRET", value: "123456")
# Subscribe and executes the code to trigger
# the `System.EnvError` exception and outputs the 'Add secret' button
Session.subscribe(session.id)
section_id = insert_section(session.pid)
code = ~s{System.fetch_env!("LB_#{secret.name}")}
@ -1173,22 +1183,30 @@ defmodule LivebookWeb.SessionLiveTest do
Session.queue_cell_evaluation(session.pid, cell_id)
assert_receive {:operation, {:add_cell_evaluation_response, _, ^cell_id, _, _}}
# Enters the session to check if the button exists
{:ok, view, _} = live(conn, "/sessions/#{session.id}")
expected_url = Routes.session_path(conn, :secrets, session.id, secret_name: secret.name)
add_secret_button = element(view, "a[href='#{expected_url}']")
assert has_element?(add_secret_button)
# Checks if the secret is persisted
assert secret in Livebook.Secrets.get_secrets()
# Clicks the button and checks if the 'Grant access' banner
# is being shown, so clicks it's button to set the app secret
# to the session, allowing the user to fetches the secret.
render_click(add_secret_button)
secrets_component = with_target(view, "#secrets-modal")
assert render(secrets_component) =~ "in your Livebook app. Allow this session to access it?"
grant_access_button = element(secrets_component, "button", "Grant access")
render_click(grant_access_button)
# Checks if the secret exists and is inside the session,
# then executes the code cell again and checks if the
# secret value is what we expected.
assert_session_secret(view, session.pid, secret)
Session.queue_cell_evaluation(session.pid, cell_id)
assert_receive {:operation,
@ -1203,8 +1221,73 @@ defmodule LivebookWeb.SessionLiveTest do
{:ok, view, _} = live(conn, "/sessions/#{session.id}")
assert render(view) =~ secret.name
assert render(view) =~ secret.value
# Subscribe and executes the code to trigger
# the `System.EnvError` exception and outputs the 'Add secret' button
Session.subscribe(session.id)
section_id = insert_section(session.pid)
code = ~s{System.fetch_env!("LB_#{secret.name}")}
cell_id = insert_text_cell(session.pid, section_id, :code, code)
# Sets the secret directly
Session.set_secret(session.pid, secret)
# Checks if the secret exists and is inside the session,
# then executes the code cell again and checks if the
# secret value is what we expected.
assert_session_secret(view, session.pid, secret)
Session.queue_cell_evaluation(session.pid, cell_id)
assert_receive {:operation,
{:add_cell_evaluation_response, _, ^cell_id, {:text, output}, _}}
assert output == "\e[32m\"#{secret.value}\"\e[0m"
end
test "granting access for unavailable startup secret using 'Add secret' button",
%{conn: conn, session: session} do
secret = build(:secret, name: "MYSTARTUPSECRET", value: "ChonkyCat", origin: :startup)
Livebook.Secrets.set_temporary_secrets([secret])
# Subscribe and executes the code to trigger
# the `System.EnvError` exception and outputs the 'Add secret' button
Session.subscribe(session.id)
section_id = insert_section(session.pid)
code = ~s{System.fetch_env!("LB_#{secret.name}")}
cell_id = insert_text_cell(session.pid, section_id, :code, code)
Session.queue_cell_evaluation(session.pid, cell_id)
assert_receive {:operation, {:add_cell_evaluation_response, _, ^cell_id, _, _}}
# Enters the session to check if the button exists
{:ok, view, _} = live(conn, "/sessions/#{session.id}")
expected_url = Routes.session_path(conn, :secrets, session.id, secret_name: secret.name)
add_secret_button = element(view, "a[href='#{expected_url}']")
assert has_element?(add_secret_button)
# Checks if the secret is persisted
assert secret in Livebook.Secrets.get_secrets()
# Clicks the button and checks if the 'Grant access' banner
# is being shown, so clicks it's button to set the app secret
# to the session, allowing the user to fetches the secret.
render_click(add_secret_button)
secrets_component = with_target(view, "#secrets-modal")
assert render(secrets_component) =~ "in your Livebook app. Allow this session to access it?"
grant_access_button = element(secrets_component, "button", "Grant access")
render_click(grant_access_button)
# Checks if the secret exists and is inside the session,
# then executes the code cell again and checks if the
# secret value is what we expected.
assert_session_secret(view, session.pid, secret)
Session.queue_cell_evaluation(session.pid, cell_id)
assert_receive {:operation,
{:add_cell_evaluation_response, _, ^cell_id, {:text, output}, _}}
assert output == "\e[32m\"#{secret.value}\"\e[0m"
end
end
@ -1303,71 +1386,4 @@ defmodule LivebookWeb.SessionLiveTest do
assert output == "\e[32m\"#{String.replace(initial_os_path, "\\", "\\\\")}\"\e[0m"
end
end
# Helpers
defp wait_for_session_update(session_pid) do
# This call is synchronous, so it gives the session time
# for handling the previously sent change messages.
Session.get_data(session_pid)
:ok
end
# Utils for sending session requests, waiting for the change to be applied
# and retrieving new ids if applicable.
defp insert_section(session_pid) do
Session.insert_section(session_pid, 0)
%{notebook: %{sections: [section]}} = Session.get_data(session_pid)
section.id
end
defp insert_text_cell(session_pid, section_id, type, content \\ "") do
Session.insert_cell(session_pid, section_id, 0, type, %{source: content})
%{notebook: %{sections: [%{cells: [cell]}]}} = Session.get_data(session_pid)
cell.id
end
defp evaluate_setup(session_pid) do
Session.queue_cell_evaluation(session_pid, "setup")
assert_receive {:operation, {:add_cell_evaluation_response, _, "setup", _, _}}
end
defp insert_cell_with_output(session_pid, section_id, output) do
code =
quote do
send(
Process.group_leader(),
{:io_request, self(), make_ref(), {:livebook_put_output, unquote(Macro.escape(output))}}
)
end
|> Macro.to_string()
cell_id = insert_text_cell(session_pid, section_id, :code, code)
Session.queue_cell_evaluation(session_pid, cell_id)
assert_receive {:operation, {:add_cell_evaluation_response, _, ^cell_id, _, _}}
cell_id
end
defp url(port), do: "http://localhost:#{port}"
defp close_session_by_id(session_id) do
{:ok, session} = Sessions.fetch_session(session_id)
Session.close(session.pid)
end
defp assert_session_secret(view, session_pid, secret) do
selector =
case secret do
%{name: name, origin: :session} -> "#session-secret-#{name}-title"
%{name: name, origin: :app} -> "#app-secret-#{name}-title"
%{name: name, origin: {:hub, id}} -> "#hub-#{id}-secret-#{name}-title"
end
assert has_element?(view, selector)
secrets = Session.get_data(session_pid).secrets
assert Map.has_key?(secrets, secret.name)
assert secrets[secret.name] == secret.value
end
end

View file

@ -23,7 +23,10 @@ defmodule Livebook.EnterpriseIntegrationCase do
end
{:ok,
url: EnterpriseServer.url(), token: EnterpriseServer.token(), user: EnterpriseServer.user()}
url: EnterpriseServer.url(),
token: EnterpriseServer.token(),
user: EnterpriseServer.user(),
node: EnterpriseServer.get_node()}
end
def start_new_instance(name) do

View file

@ -0,0 +1,71 @@
defmodule Livebook.SessionHelpers do
@moduledoc false
alias Livebook.{Session, Sessions}
import ExUnit.Assertions
import Phoenix.LiveViewTest
def wait_for_session_update(session_pid) do
# This call is synchronous, so it gives the session time
# for handling the previously sent change messages.
Session.get_data(session_pid)
:ok
end
def evaluate_setup(session_pid) do
Session.queue_cell_evaluation(session_pid, "setup")
assert_receive {:operation, {:add_cell_evaluation_response, _, "setup", _, _}}
end
def insert_cell_with_output(session_pid, section_id, output) do
code =
quote do
send(
Process.group_leader(),
{:io_request, self(), make_ref(), {:livebook_put_output, unquote(Macro.escape(output))}}
)
end
|> Macro.to_string()
cell_id = insert_text_cell(session_pid, section_id, :code, code)
Session.queue_cell_evaluation(session_pid, cell_id)
assert_receive {:operation, {:add_cell_evaluation_response, _, ^cell_id, _, _}}
cell_id
end
def bypass_url(port), do: "http://localhost:#{port}"
def close_session_by_id(session_id) do
{:ok, session} = Sessions.fetch_session(session_id)
Session.close(session.pid)
end
def insert_section(session_pid) do
Session.insert_section(session_pid, 0)
%{notebook: %{sections: [section]}} = Session.get_data(session_pid)
section.id
end
def insert_text_cell(session_pid, section_id, type, content \\ " ") do
Session.insert_cell(session_pid, section_id, 0, type, %{source: content})
%{notebook: %{sections: [%{cells: [cell]}]}} = Session.get_data(session_pid)
cell.id
end
def assert_session_secret(view, session_pid, secret) do
selector =
case secret do
%{name: name, origin: :session} -> "#session-secret-#{name}-title"
%{name: name, origin: :app} -> "#app-secret-#{name}-title"
%{name: name, origin: :startup} -> "#startup-secret-#{name}-title"
%{name: name, origin: {:hub, id}} -> "#hub-#{id}-secret-#{name}-title"
end
assert has_element?(view, selector)
secrets = Session.get_data(session_pid).secrets
assert Map.has_key?(secrets, secret.name)
assert secrets[secret.name] == secret.value
end
end