Explicitly track which secrets come from the hub (#1769)

This commit is contained in:
Jonatan Kłosko 2023-03-11 12:51:06 +01:00 committed by GitHub
parent 6e53c78597
commit deacb1a4a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 197 additions and 138 deletions

View file

@ -194,12 +194,12 @@ defmodule Livebook.Application do
%Livebook.Secrets.Secret{
name: name,
value: value,
hub_id: Livebook.Hubs.Personal.id(),
hub_id: nil,
readonly: true
}
end
Livebook.Hubs.Personal.set_startup_secrets(secrets)
Livebook.Secrets.set_startup_secrets(secrets)
end
defp config_env_var?("LIVEBOOK_" <> _), do: true

View file

@ -46,8 +46,8 @@ defmodule Livebook.Hubs do
@doc """
Gets one hub from storage.
"""
@spec get_hub(String.t()) :: {:ok, Provider.t()} | :error
def get_hub(id) do
@spec fetch_hub(String.t()) :: {:ok, Provider.t()} | :error
def fetch_hub(id) do
with {:ok, data} <- Storage.fetch(@namespace, id) do
{:ok, to_struct(data)}
end
@ -92,7 +92,7 @@ defmodule Livebook.Hubs do
"""
@spec delete_hub(String.t()) :: :ok
def delete_hub(id) do
with {:ok, hub} <- get_hub(id) do
with {:ok, hub} <- fetch_hub(id) do
true = Provider.type(hub) != "personal"
:ok = Broadcasts.hub_changed()
:ok = Storage.delete(@namespace, id)
@ -211,7 +211,7 @@ defmodule Livebook.Hubs do
@spec get_secrets(Provider.t()) :: list(Secret.t())
def get_secrets(hub) do
if capability?(hub, [:list_secrets]) do
hub |> Provider.get_secrets() |> Enum.sort()
Provider.get_secrets(hub)
else
[]
end

View file

@ -68,24 +68,6 @@ defmodule Livebook.Hubs.Personal do
|> put_change(:id, id())
end
@secret_startup_key :livebook_startup_secrets
@doc """
Get the startup secrets list from persistent term.
"""
@spec get_startup_secrets() :: list(Secret.t())
def get_startup_secrets do
:persistent_term.get(@secret_startup_key, [])
end
@doc """
Sets additional secrets that are kept only in memory.
"""
@spec set_startup_secrets(list(Secret.t())) :: :ok
def set_startup_secrets(secrets) do
:persistent_term.put(@secret_startup_key, secrets)
end
@doc """
Generates a random secret key used for stamping the notebook.
"""
@ -128,7 +110,7 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Personal do
def capabilities(_personal), do: ~w(list_secrets create_secret)a
def get_secrets(personal) do
Secrets.get_secrets(personal) ++ Livebook.Hubs.Personal.get_startup_secrets()
Secrets.get_secrets(personal)
end
def create_secret(_personal, secret) do

View file

@ -321,7 +321,7 @@ defmodule Livebook.LiveMarkdown.Export do
defp render_notebook_footer(notebook, notebook_source) do
metadata = notebook_stamp_metadata(notebook)
with {:ok, hub} <- Livebook.Hubs.get_hub(notebook.hub_id),
with {:ok, hub} <- Livebook.Hubs.fetch_hub(notebook.hub_id),
{:ok, stamp} <- Livebook.Hubs.notebook_stamp(hub, notebook_source, metadata) do
offset = IO.iodata_length(notebook_source)
json = Jason.encode!(%{"offset" => offset, "stamp" => stamp})

View file

@ -114,4 +114,22 @@ defmodule Livebook.Secrets do
hub_id = fields[:hub_id] || Livebook.Hubs.Personal.id()
hub_id == hub.id
end
@secret_startup_key :livebook_startup_secrets
@doc """
Get the startup secrets list from persistent term.
"""
@spec get_startup_secrets() :: list(Secret.t())
def get_startup_secrets do
:persistent_term.get(@secret_startup_key, [])
end
@doc """
Sets additional secrets that are kept only in memory.
"""
@spec set_startup_secrets(list(Secret.t())) :: :ok
def set_startup_secrets(secrets) do
:persistent_term.put(@secret_startup_key, secrets)
end
end

View file

@ -24,6 +24,6 @@ defmodule Livebook.Secrets.Secret do
|> validate_format(:name, ~r/^\w+$/,
message: "should contain only alphanumeric characters and underscore"
)
|> validate_required([:name, :value, :hub_id])
|> validate_required([:name, :value])
end
end

View file

@ -2042,13 +2042,13 @@ defmodule Livebook.Session do
end
defp set_runtime_secrets(state, secrets) do
secrets = Enum.map(secrets, fn {name, value} -> {"LB_#{name}", value} end)
Runtime.put_system_envs(state.data.runtime, secrets)
envs_vars = Enum.map(secrets, fn {_name, secret} -> {"LB_#{secret.name}", secret.value} end)
Runtime.put_system_envs(state.data.runtime, envs_vars)
end
defp delete_runtime_secrets(state, secret_names) do
secret_names = Enum.map(secret_names, &"LB_#{&1}")
Runtime.delete_system_envs(state.data.runtime, secret_names)
env_var_names = Enum.map(secret_names, &"LB_#{&1}")
Runtime.delete_system_envs(state.data.runtime, env_var_names)
end
defp set_runtime_env_vars(state) do

View file

@ -42,6 +42,7 @@ defmodule Livebook.Session.Data do
alias Livebook.Users.User
alias Livebook.Notebook.{Cell, Section, AppSettings}
alias Livebook.Utils.Graph
alias Livebook.Secrets.Secret
@type t :: %__MODULE__{
notebook: Notebook.t(),
@ -56,8 +57,8 @@ defmodule Livebook.Session.Data do
smart_cell_definitions: list(Runtime.smart_cell_definition()),
clients_map: %{client_id() => User.id()},
users_map: %{User.id() => User.t()},
secrets: %{(name :: String.t()) => value :: String.t()},
hub_secrets: list(Livebook.Secrets.Secret.t()),
secrets: secrets(),
hub_secrets: list(Secret.t()),
mode: session_mode(),
apps: list(app()),
app_data: nil | app_data()
@ -135,8 +136,6 @@ defmodule Livebook.Session.Data do
@type index :: non_neg_integer()
@type secret :: %{name: String.t(), value: String.t()}
# Snapshot holds information about the cell evaluation dependencies,
# including parent cells and inputs. Whenever the snapshot changes,
# it implies a new evaluation context, which basically means the cell
@ -145,6 +144,8 @@ defmodule Livebook.Session.Data do
@type input_reading :: {input_id(), input_value :: term()}
@type secrets :: %{(name :: String.t()) => Secret.t()}
@type session_mode :: :default | :app
@type app :: %{
@ -211,7 +212,7 @@ defmodule Livebook.Session.Data do
| {:set_file, client_id(), FileSystem.File.t() | nil}
| {:set_autosave_interval, client_id(), non_neg_integer() | nil}
| {:mark_as_not_dirty, client_id()}
| {:set_secret, client_id(), secret()}
| {:set_secret, client_id(), Secret.t()}
| {:unset_secret, client_id(), String.t()}
| {:set_notebook_hub, client_id(), String.t()}
| {:sync_hub_secrets, client_id()}
@ -264,11 +265,16 @@ defmodule Livebook.Session.Data do
hub = Hubs.fetch_hub!(notebook.hub_id)
hub_secrets = Hubs.get_secrets(hub)
startup_secrets =
for secret <- Livebook.Secrets.get_startup_secrets(),
do: {secret.name, secret},
into: %{}
secrets =
for secret <- hub_secrets,
secret.name in notebook.hub_secret_names,
do: {secret.name, secret.value},
into: %{}
do: {secret.name, secret},
into: startup_secrets
data = %__MODULE__{
notebook: notebook,
@ -852,7 +858,7 @@ defmodule Livebook.Session.Data do
end
def apply_operation(data, {:set_notebook_hub, _client_id, id}) do
with {:ok, hub} <- Hubs.get_hub(id) do
with {:ok, hub} <- Hubs.fetch_hub(id) do
data
|> with_actions()
|> set_notebook_hub(hub)
@ -1664,7 +1670,7 @@ defmodule Livebook.Session.Data do
defp update_notebook_hub_secret_names({data, _} = data_actions) do
hub_secret_names =
for secret <- data.hub_secrets, data.secrets[secret.name] == secret.value, do: secret.name
for {_name, secret} <- data.secrets, secret.hub_id == data.notebook.hub_id, do: secret.name
set!(data_actions, notebook: %{data.notebook | hub_secret_names: hub_secret_names})
end
@ -1807,7 +1813,7 @@ defmodule Livebook.Session.Data do
end
defp set_secret({data, _} = data_actions, secret) do
secrets = Map.put(data.secrets, secret.name, secret.value)
secrets = Map.put(data.secrets, secret.name, secret)
set!(data_actions, secrets: secrets)
end
@ -2597,4 +2603,42 @@ defmodule Livebook.Session.Data do
Map.fetch(data.input_values, input_id)
end
@doc """
Returns a list of secrets that don't belong to the given hub
and are effectively stored in the session only.
"""
@spec session_secrets(secrets(), String.t()) :: list(Secret.t())
def session_secrets(secrets, hub_id) do
for {_name, secret} <- secrets, secret.hub_id != hub_id, do: secret
end
@doc """
Checks whether the given hub secret is present in secrets.
"""
@spec secret_toggled?(Secret.t(), secrets()) :: boolean()
def secret_toggled?(secret, secrets) do
Map.has_key?(secrets, secret.name) and secrets[secret.name].hub_id == secret.hub_id
end
@doc """
Checks whether the given hub secret is present in secrets and has
old value.
"""
@spec secret_outdated?(Secret.t(), secrets()) :: boolean()
def secret_outdated?(secret, secrets) do
secret_used_value(secret, secrets) != secret.value
end
@doc """
Returns currently used hub secret value (or actual value if not used).
"""
@spec secret_used_value(Secret.t(), secrets()) :: String.t()
def secret_used_value(secret, secrets) do
if secret_toggled?(secret, secrets) do
secrets[secret.name].value
else
secret.value
end
end
end

View file

@ -9,7 +9,7 @@ defmodule LivebookWeb.SessionLive do
alias Livebook.Notebook.{Cell, ContentLoader}
alias Livebook.JSInterop
on_mount LivebookWeb.SidebarHook
on_mount(LivebookWeb.SidebarHook)
@impl true
def mount(%{"id" => session_id}, _session, socket) do
@ -196,9 +196,9 @@ defmodule LivebookWeb.SessionLive do
module={LivebookWeb.SessionLive.SecretsListComponent}
id="secrets-list"
session={@session}
saved_secrets={@data_view.hub_secrets}
hub={@data_view.hub}
secrets={@data_view.secrets}
hub_secrets={@data_view.hub_secrets}
hub={@data_view.hub}
/>
</div>
<div data-el-app-info>
@ -514,8 +514,8 @@ defmodule LivebookWeb.SessionLive do
id="secrets"
session={@session}
secrets={@data_view.secrets}
hub_secrets={@data_view.hub_secrets}
hub={@data_view.hub}
saved_secrets={@data_view.hub_secrets}
prefill_secret_name={@prefill_secret_name}
select_secret_ref={@select_secret_ref}
select_secret_options={@select_secret_options}

View file

@ -20,13 +20,13 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
socket =
socket
|> assign_new(:changeset, fn ->
attrs = %{name: secret_name, value: nil, hub_id: "session", readonly: false}
attrs = %{name: secret_name, value: nil, hub_id: nil, readonly: false}
Secrets.change_secret(%Secret{}, attrs)
end)
|> assign_new(:grant_access_secret, fn ->
Enum.find(
socket.assigns.saved_secrets,
&(&1.name == secret_name and secret_name not in Map.keys(socket.assigns.secrets))
socket.assigns.hub_secrets,
&(&1.name == secret_name and not is_map_key(socket.assigns.secrets, secret_name))
)
end)
@ -54,23 +54,29 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
</p>
<div class="flex flex-wrap">
<.secret_with_badge
:for={{secret_name, _} <- Enum.sort(@secrets)}
secret_name={secret_name}
secret_origin={:session}
:for={
secret <-
@secrets |> Session.Data.session_secrets(@hub.id) |> Enum.sort_by(& &1.name)
}
secret_name={secret.name}
hub?={false}
stored="Session"
active={secret_name == @prefill_secret_name}
active={secret.name == @prefill_secret_name}
target={@myself}
/>
<.secret_with_badge
:for={secret <- @saved_secrets}
:if={!is_map_key(@secrets, secret.name) and @secrets[secret.name] != secret.value}
:for={secret <- Enum.sort_by(@hub_secrets, & &1.name)}
secret_name={secret.name}
hub?={true}
stored={hub_label(@hub)}
active={false}
active={
secret.name == @prefill_secret_name and
Session.Data.secret_toggled?(secret, @secrets)
}
target={@myself}
/>
<div
:if={@secrets == %{} and @saved_secrets == []}
:if={@secrets == %{} and @hub_secrets == []}
class="w-full text-center text-gray-400 border rounded-lg p-8"
>
<.remix_icon icon="folder-lock-line" class="align-middle text-2xl" />
@ -115,7 +121,7 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
field={f[:hub_id]}
label="Storage"
options={[
{"session", "only this session"},
{"", "only this session"},
{@hub.id, "in #{@hub.hub_emoji} #{@hub.hub_name}"}
]}
/>
@ -136,8 +142,6 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
end
defp secret_with_badge(assigns) do
assigns = assign_new(assigns, :secret_origin, fn -> nil end)
~H"""
<div
role="button"
@ -149,10 +153,10 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
"text-gray-700 hover:bg-gray-100"
end
]}
phx-click="select_secret"
phx-value-name={@secret_name}
phx-value-origin={@secret_origin}
phx-value-hub={@hub?}
phx-target={@target}
phx-click="grant_access"
>
<%= @secret_name %>
<span class={[
@ -196,9 +200,9 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
</div>
<button
class="button-base button-gray"
phx-click="grant_access"
phx-click="select_secret"
phx-value-name={@secret.name}
phx-value-hub_id={@secret.hub_id}
phx-value-hub={true}
phx-target={@target}
>
Grant access
@ -233,16 +237,13 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
{:noreply, assign(socket, changeset: changeset)}
end
def handle_event("grant_access", %{"name" => secret_name} = attrs, socket) do
cond do
attrs["origin"] == "session" and is_map_key(socket.assigns.secrets, secret_name) ->
Session.set_secret(socket.assigns.session.pid, %{
name: secret_name,
value: socket.assigns.secrets[secret_name]
})
def handle_event("select_secret", %{"name" => secret_name} = attrs, socket) do
if attrs["hub"] do
secret = Enum.find(socket.assigns.hub_secrets, &(&1.name == secret_name))
secret = Enum.find(socket.assigns.saved_secrets, &(&1.name == secret_name)) ->
unless Session.Data.secret_toggled?(secret, socket.assigns.secrets) do
Session.set_secret(socket.assigns.session.pid, secret)
end
end
{:noreply,
@ -261,7 +262,7 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
defp title(%{assigns: %{select_secret_options: %{"title" => title}}}), do: title
defp title(_), do: "Select secret"
defp set_secret(socket, %Secret{hub_id: "session"} = secret) do
defp set_secret(socket, %Secret{hub_id: nil} = secret) do
Session.set_secret(socket.assigns.session.pid, secret)
end

View file

@ -2,9 +2,9 @@ defmodule LivebookWeb.SessionLive.SecretsListComponent do
use LivebookWeb, :live_component
alias Livebook.Hubs
alias Livebook.Session
alias Livebook.Secrets
alias Livebook.Secrets.Secret
alias Livebook.Session
@impl true
def render(assigns) do
@ -20,36 +20,38 @@ defmodule LivebookWeb.SessionLive.SecretsListComponent do
<div class="flex flex-col">
<div class="flex flex-col space-y-4 mt-6">
<div
:for={{secret_name, secret_value} <- Enum.sort(@secrets)}
:for={
secret <- @secrets |> Session.Data.session_secrets(@hub.id) |> Enum.sort_by(& &1.name)
}
class="flex flex-col text-gray-500 rounded-lg px-2 pt-1"
id={"session-secret-#{secret_name}-wrapper"}
id={"session-secret-#{secret.name}"}
>
<span
class="text-sm font-mono break-all flex-row cursor-pointer"
phx-click={
JS.toggle(to: "#session-secret-#{secret_name}-detail", display: "flex")
|> toggle_class("bg-gray-100", to: "#session-secret-#{secret_name}-wrapper")
JS.toggle(to: "#session-secret-#{secret.name}-detail", display: "flex")
|> toggle_class("bg-gray-100", to: "#session-secret-#{secret.name}")
}
>
<%= secret_name %>
<%= secret.name %>
</span>
<div
class="flex flex-row justify-between items-center my-1 hidden"
id={"session-secret-#{secret_name}-detail"}
id={"session-secret-#{secret.name}-detail"}
>
<span class="text-sm font-mono break-all flex-row">
<%= secret_value %>
<%= secret.value %>
</span>
<button
id={"session-secret-#{secret_name}-delete"}
id={"session-secret-#{secret.name}-delete"}
type="button"
phx-click={
with_confirm(
JS.push("delete_session_secret",
value: %{secret_name: secret_name},
value: %{secret_name: secret.name},
target: @myself
),
title: "Delete session secret - #{secret_name}",
title: "Delete session secret - #{secret.name}",
description: "Are you sure you want to delete this session secret?",
confirm_text: "Delete",
confirm_icon: "delete-bin-6-line"
@ -77,7 +79,7 @@ defmodule LivebookWeb.SessionLive.SecretsListComponent do
<%= @hub.hub_emoji %> <%= @hub.hub_name %> secrets
</h3>
<span class="text-sm text-gray-500">
<%= if @saved_secrets == [] do %>
<%= if @hub_secrets == [] do %>
No secrets stored in Livebook so far
<% else %>
Toggle to share with this session
@ -87,10 +89,10 @@ defmodule LivebookWeb.SessionLive.SecretsListComponent do
<div class="flex flex-col space-y-4 mt-6">
<.secrets_item
:for={secret <- @saved_secrets}
:for={secret <- Enum.sort_by(@hub_secrets, & &1.name)}
id={"hub-#{secret.hub_id}-secret-#{secret.name}"}
secret={secret}
prefix={"hub-#{secret.hub_id}"}
data_secrets={@secrets}
secrets={@secrets}
hub={@hub}
myself={@myself}
/>
@ -102,45 +104,58 @@ defmodule LivebookWeb.SessionLive.SecretsListComponent do
defp secrets_item(assigns) do
~H"""
<div
class="flex flex-col text-gray-500 rounded-lg px-2 pt-1"
id={"#{@prefix}-secret-#{@secret.name}-wrapper"}
>
<div class="flex flex-col text-gray-500 rounded-lg px-2 pt-1" id={@id}>
<div class="flex flex-col text-gray-800">
<div class="flex flex-col">
<div class="flex justify-between items-center">
<span
class="text-sm font-mono w-full break-all flex-row cursor-pointer"
phx-click={
JS.toggle(to: "##{@prefix}-secret-#{@secret.name}-detail", display: "flex")
|> toggle_class("bg-gray-100", to: "##{@prefix}-secret-#{@secret.name}-wrapper")
JS.toggle(to: "##{@id}-detail", display: "flex")
|> toggle_class("bg-gray-100", to: "##{@id}")
}
>
<%= @secret.name %>
</span>
<span
class="mr-2 tooltip bottom-left"
data-tooltip={
~S'''
The secret value changed,
click to load the latest one.
'''
}
>
<button
:if={Session.Data.secret_outdated?(@secret, @secrets)}
class="icon-button"
aria-label="load latest value"
phx-click={
JS.push("update_outdated", value: %{"name" => @secret.name}, target: @myself)
}
>
<.remix_icon icon="refresh-line" class="text-xl leading-none" />
</button>
</span>
<.form
:let={f}
id={"#{@prefix}-secret-#{@secret.name}-toggle"}
for={%{"toggled" => secret_toggled?(@secret, @data_secrets)}}
id={"#{@id}-toggle"}
for={%{"toggled" => Session.Data.secret_toggled?(@secret, @secrets)}}
as={:data}
phx-change="toggle_secret"
phx-target={@myself}
>
<.switch_field field={f[:toggled]} />
<.hidden_field field={f[:name]} value={@secret.name} />
<.hidden_field field={f[:value]} value={@secret.value} />
</.form>
</div>
<div
class="flex flex-row justify-between items-center my-1 hidden"
id={"#{@prefix}-secret-#{@secret.name}-detail"}
>
<div class="flex flex-row justify-between items-center my-1 hidden" id={"#{@id}-detail"}>
<span class="text-sm font-mono break-all flex-row">
<%= @secret.value %>
<%= Session.Data.secret_used_value(@secret, @secrets) %>
</span>
<button
:if={!@secret.readonly}
id={"#{@prefix}-secret-#{@secret.name}-delete"}
id={"#{@id}-delete"}
type="button"
phx-click={
with_confirm(
@ -190,7 +205,7 @@ defmodule LivebookWeb.SessionLive.SecretsListComponent do
@impl true
def handle_event("toggle_secret", %{"data" => data}, socket) do
if data["toggled"] == "true" do
secret = %{name: data["name"], value: data["value"]}
secret = Enum.find(socket.assigns.hub_secrets, &(&1.name == data["name"]))
Session.set_secret(socket.assigns.session.pid, secret)
else
Session.unset_secret(socket.assigns.session.pid, data["name"])
@ -199,6 +214,13 @@ defmodule LivebookWeb.SessionLive.SecretsListComponent do
{:noreply, socket}
end
def handle_event("update_outdated", %{"name" => name}, socket) do
secret = Enum.find(socket.assigns.hub_secrets, &(&1.name == name))
Session.set_secret(socket.assigns.session.pid, secret)
{:noreply, socket}
end
def handle_event("delete_session_secret", %{"secret_name" => secret_name}, socket) do
Session.unset_secret(socket.assigns.session.pid, secret_name)
{:noreply, socket}
@ -211,8 +233,4 @@ defmodule LivebookWeb.SessionLive.SecretsListComponent do
{:noreply, socket}
end
defp secret_toggled?(secret, secrets) do
Map.has_key?(secrets, secret.name) and secrets[secret.name] == secret.value
end
end

View file

@ -34,18 +34,6 @@ defmodule Livebook.Hubs.ProviderTest do
assert secret in Provider.get_secrets(hub)
end
test "get_secrets/1 with startup secrets", %{hub: hub} do
# Set in test_helper.exs
secret = %Livebook.Secrets.Secret{
name: "STARTUP_SECRET",
value: "value",
hub_id: Livebook.Hubs.Personal.id(),
readonly: true
}
assert secret in Provider.get_secrets(hub)
end
test "create_secret/1", %{hub: hub} do
secret = build(:secret, name: "CREATE_PERSONAL_SECRET")

View file

@ -75,7 +75,7 @@ defmodule LivebookWeb.Hub.EditLiveTest do
refute hubs_html =~ ~p"/hub/#{hub.id}"
refute hubs_html =~ hub.hub_name
assert Hubs.get_hub(hub_id) == :error
assert Hubs.fetch_hub(hub_id) == :error
end
test "add env var", %{conn: conn, bypass: bypass} do

View file

@ -1075,7 +1075,7 @@ defmodule LivebookWeb.SessionLiveTest do
test "adds a secret from form", %{conn: conn, session: session} do
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}/secrets")
secret = build(:secret, name: "FOO", value: "123", hub_id: "session")
secret = build(:secret, name: "FOO", value: "123", hub_id: nil)
view
|> element(~s{form[phx-submit="save"]})
@ -1130,7 +1130,7 @@ defmodule LivebookWeb.SessionLiveTest do
test "never syncs secrets when updating from session",
%{conn: conn, session: session, hub: hub} do
hub_secret = insert_secret(name: "FOO", value: "123")
secret = build(:secret, name: "FOO", value: "456", hub_id: "session")
secret = build(:secret, name: "FOO", value: "456", hub_id: nil)
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}/secrets")
Session.set_secret(session.pid, hub_secret)
@ -1145,7 +1145,7 @@ defmodule LivebookWeb.SessionLiveTest do
end
test "shows the 'Add secret' button for missing secrets", %{conn: conn, session: session} do
secret = build(:secret, name: "ANOTHER_GREAT_SECRET", value: "123456", hub_id: "session")
secret = build(:secret, name: "ANOTHER_GREAT_SECRET", value: "123456", hub_id: nil)
Session.subscribe(session.id)
section_id = insert_section(session.pid)
code = ~s{System.fetch_env!("LB_#{secret.name}")}
@ -1163,7 +1163,7 @@ defmodule LivebookWeb.SessionLiveTest do
test "adding a missing secret using 'Add secret' button",
%{conn: conn, session: session, hub: hub} do
secret = build(:secret, name: "MYUNAVAILABLESECRET", value: "123456", hub_id: "session")
secret = build(:secret, name: "MYUNAVAILABLESECRET", value: "123456", hub_id: nil)
# Subscribe and executes the code to trigger
# the `System.EnvError` exception and outputs the 'Add secret' button
@ -1250,6 +1250,25 @@ defmodule LivebookWeb.SessionLiveTest do
assert output == "\e[32m\"#{secret.value}\"\e[0m"
end
test "reloading outdated secret value", %{conn: conn, session: session} do
hub_secret = insert_secret(name: "FOO", value: "123")
Session.set_secret(session.pid, hub_secret)
{:ok, updated_hub_secret} = Livebook.Secrets.update_secret(hub_secret, %{value: "456"})
hub = Livebook.Hubs.fetch_hub!(hub_secret.hub_id)
:ok = Livebook.Hubs.update_secret(hub, updated_hub_secret)
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}/secrets")
assert_session_secret(view, session.pid, hub_secret)
view
|> element(~s{button[aria-label="load latest value"]})
|> render_click()
assert_session_secret(view, session.pid, updated_hub_secret)
end
end
describe "environment variables" do

View file

@ -57,15 +57,14 @@ defmodule Livebook.SessionHelpers do
def assert_session_secret(view, session_pid, secret) do
selector =
case secret do
%{name: name, hub_id: "session"} -> "#session-secret-#{name}-wrapper"
%{name: name, hub_id: id} -> "#hub-#{id}-secret-#{name}-wrapper"
%{name: name, hub_id: nil} -> "#session-secret-#{name}"
%{name: name, hub_id: id} -> "#hub-#{id}-secret-#{name}"
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
assert secrets[secret.name] == secret
end
def hub_label(%Secret{hub_id: id}), do: hub_label(Hubs.fetch_hub!(id))

View file

@ -40,16 +40,6 @@ Application.put_env(:livebook, Livebook.Runtime.Embedded,
# Disable autosaving
Livebook.Storage.insert(:settings, "global", autosave_path: nil)
# Set a global startup secret, so that there is at least one
Livebook.Hubs.Personal.set_startup_secrets([
%Livebook.Secrets.Secret{
name: "STARTUP_SECRET",
value: "value",
hub_id: Livebook.Hubs.Personal.id(),
readonly: true
}
])
# Always use the same secret key in tests
secret_key =
"5ji8DpnX761QAWXZwSl-2Y-mdW4yTcMimdOJ8SSxCh44wFE0jEbGBUf-VydKwnTLzBiAUedQKs3X_q1j_3lgrw"