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{ %Livebook.Secrets.Secret{
name: name, name: name,
value: value, value: value,
hub_id: Livebook.Hubs.Personal.id(), hub_id: nil,
readonly: true readonly: true
} }
end end
Livebook.Hubs.Personal.set_startup_secrets(secrets) Livebook.Secrets.set_startup_secrets(secrets)
end end
defp config_env_var?("LIVEBOOK_" <> _), do: true defp config_env_var?("LIVEBOOK_" <> _), do: true

View file

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

View file

@ -68,24 +68,6 @@ defmodule Livebook.Hubs.Personal do
|> put_change(:id, id()) |> put_change(:id, id())
end 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 """ @doc """
Generates a random secret key used for stamping the notebook. 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 capabilities(_personal), do: ~w(list_secrets create_secret)a
def get_secrets(personal) do def get_secrets(personal) do
Secrets.get_secrets(personal) ++ Livebook.Hubs.Personal.get_startup_secrets() Secrets.get_secrets(personal)
end end
def create_secret(_personal, secret) do 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 defp render_notebook_footer(notebook, notebook_source) do
metadata = notebook_stamp_metadata(notebook) 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 {:ok, stamp} <- Livebook.Hubs.notebook_stamp(hub, notebook_source, metadata) do
offset = IO.iodata_length(notebook_source) offset = IO.iodata_length(notebook_source)
json = Jason.encode!(%{"offset" => offset, "stamp" => stamp}) 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 = fields[:hub_id] || Livebook.Hubs.Personal.id()
hub_id == hub.id hub_id == hub.id
end 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 end

View file

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

View file

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

View file

@ -42,6 +42,7 @@ defmodule Livebook.Session.Data do
alias Livebook.Users.User alias Livebook.Users.User
alias Livebook.Notebook.{Cell, Section, AppSettings} alias Livebook.Notebook.{Cell, Section, AppSettings}
alias Livebook.Utils.Graph alias Livebook.Utils.Graph
alias Livebook.Secrets.Secret
@type t :: %__MODULE__{ @type t :: %__MODULE__{
notebook: Notebook.t(), notebook: Notebook.t(),
@ -56,8 +57,8 @@ defmodule Livebook.Session.Data do
smart_cell_definitions: list(Runtime.smart_cell_definition()), smart_cell_definitions: list(Runtime.smart_cell_definition()),
clients_map: %{client_id() => User.id()}, clients_map: %{client_id() => User.id()},
users_map: %{User.id() => User.t()}, users_map: %{User.id() => User.t()},
secrets: %{(name :: String.t()) => value :: String.t()}, secrets: secrets(),
hub_secrets: list(Livebook.Secrets.Secret.t()), hub_secrets: list(Secret.t()),
mode: session_mode(), mode: session_mode(),
apps: list(app()), apps: list(app()),
app_data: nil | app_data() app_data: nil | app_data()
@ -135,8 +136,6 @@ defmodule Livebook.Session.Data do
@type index :: non_neg_integer() @type index :: non_neg_integer()
@type secret :: %{name: String.t(), value: String.t()}
# Snapshot holds information about the cell evaluation dependencies, # Snapshot holds information about the cell evaluation dependencies,
# including parent cells and inputs. Whenever the snapshot changes, # including parent cells and inputs. Whenever the snapshot changes,
# it implies a new evaluation context, which basically means the cell # 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 input_reading :: {input_id(), input_value :: term()}
@type secrets :: %{(name :: String.t()) => Secret.t()}
@type session_mode :: :default | :app @type session_mode :: :default | :app
@type app :: %{ @type app :: %{
@ -211,7 +212,7 @@ defmodule Livebook.Session.Data do
| {:set_file, client_id(), FileSystem.File.t() | nil} | {:set_file, client_id(), FileSystem.File.t() | nil}
| {:set_autosave_interval, client_id(), non_neg_integer() | nil} | {:set_autosave_interval, client_id(), non_neg_integer() | nil}
| {:mark_as_not_dirty, client_id()} | {:mark_as_not_dirty, client_id()}
| {:set_secret, client_id(), secret()} | {:set_secret, client_id(), Secret.t()}
| {:unset_secret, client_id(), String.t()} | {:unset_secret, client_id(), String.t()}
| {:set_notebook_hub, client_id(), String.t()} | {:set_notebook_hub, client_id(), String.t()}
| {:sync_hub_secrets, client_id()} | {:sync_hub_secrets, client_id()}
@ -264,11 +265,16 @@ defmodule Livebook.Session.Data do
hub = Hubs.fetch_hub!(notebook.hub_id) hub = Hubs.fetch_hub!(notebook.hub_id)
hub_secrets = Hubs.get_secrets(hub) hub_secrets = Hubs.get_secrets(hub)
startup_secrets =
for secret <- Livebook.Secrets.get_startup_secrets(),
do: {secret.name, secret},
into: %{}
secrets = secrets =
for secret <- hub_secrets, for secret <- hub_secrets,
secret.name in notebook.hub_secret_names, secret.name in notebook.hub_secret_names,
do: {secret.name, secret.value}, do: {secret.name, secret},
into: %{} into: startup_secrets
data = %__MODULE__{ data = %__MODULE__{
notebook: notebook, notebook: notebook,
@ -852,7 +858,7 @@ defmodule Livebook.Session.Data do
end end
def apply_operation(data, {:set_notebook_hub, _client_id, id}) do 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 data
|> with_actions() |> with_actions()
|> set_notebook_hub(hub) |> set_notebook_hub(hub)
@ -1664,7 +1670,7 @@ defmodule Livebook.Session.Data do
defp update_notebook_hub_secret_names({data, _} = data_actions) do defp update_notebook_hub_secret_names({data, _} = data_actions) do
hub_secret_names = 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}) set!(data_actions, notebook: %{data.notebook | hub_secret_names: hub_secret_names})
end end
@ -1807,7 +1813,7 @@ defmodule Livebook.Session.Data do
end end
defp set_secret({data, _} = data_actions, secret) do 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) set!(data_actions, secrets: secrets)
end end
@ -2597,4 +2603,42 @@ defmodule Livebook.Session.Data do
Map.fetch(data.input_values, input_id) Map.fetch(data.input_values, input_id)
end 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 end

View file

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

View file

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

View file

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

View file

@ -34,18 +34,6 @@ defmodule Livebook.Hubs.ProviderTest do
assert secret in Provider.get_secrets(hub) assert secret in Provider.get_secrets(hub)
end 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 test "create_secret/1", %{hub: hub} do
secret = build(:secret, name: "CREATE_PERSONAL_SECRET") 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 =~ ~p"/hub/#{hub.id}"
refute hubs_html =~ hub.hub_name refute hubs_html =~ hub.hub_name
assert Hubs.get_hub(hub_id) == :error assert Hubs.fetch_hub(hub_id) == :error
end end
test "add env var", %{conn: conn, bypass: bypass} do 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 test "adds a secret from form", %{conn: conn, session: session} do
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}/secrets") {: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 view
|> element(~s{form[phx-submit="save"]}) |> element(~s{form[phx-submit="save"]})
@ -1130,7 +1130,7 @@ defmodule LivebookWeb.SessionLiveTest do
test "never syncs secrets when updating from session", test "never syncs secrets when updating from session",
%{conn: conn, session: session, hub: hub} do %{conn: conn, session: session, hub: hub} do
hub_secret = insert_secret(name: "FOO", value: "123") 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") {:ok, view, _} = live(conn, ~p"/sessions/#{session.id}/secrets")
Session.set_secret(session.pid, hub_secret) Session.set_secret(session.pid, hub_secret)
@ -1145,7 +1145,7 @@ defmodule LivebookWeb.SessionLiveTest do
end end
test "shows the 'Add secret' button for missing 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", hub_id: "session") secret = build(:secret, name: "ANOTHER_GREAT_SECRET", value: "123456", hub_id: nil)
Session.subscribe(session.id) Session.subscribe(session.id)
section_id = insert_section(session.pid) section_id = insert_section(session.pid)
code = ~s{System.fetch_env!("LB_#{secret.name}")} 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", test "adding a missing secret using 'Add secret' button",
%{conn: conn, session: session, hub: hub} do %{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 # Subscribe and executes the code to trigger
# the `System.EnvError` exception and outputs the 'Add secret' button # 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" assert output == "\e[32m\"#{secret.value}\"\e[0m"
end 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 end
describe "environment variables" do describe "environment variables" do

View file

@ -57,15 +57,14 @@ defmodule Livebook.SessionHelpers do
def assert_session_secret(view, session_pid, secret) do def assert_session_secret(view, session_pid, secret) do
selector = selector =
case secret do case secret do
%{name: name, hub_id: "session"} -> "#session-secret-#{name}-wrapper" %{name: name, hub_id: nil} -> "#session-secret-#{name}"
%{name: name, hub_id: id} -> "#hub-#{id}-secret-#{name}-wrapper" %{name: name, hub_id: id} -> "#hub-#{id}-secret-#{name}"
end end
assert has_element?(view, selector) assert has_element?(view, selector)
secrets = Session.get_data(session_pid).secrets secrets = Session.get_data(session_pid).secrets
assert Map.has_key?(secrets, secret.name) assert secrets[secret.name] == secret
assert secrets[secret.name] == secret.value
end end
def hub_label(%Secret{hub_id: id}), do: hub_label(Hubs.fetch_hub!(id)) 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 # Disable autosaving
Livebook.Storage.insert(:settings, "global", autosave_path: nil) 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 # Always use the same secret key in tests
secret_key = secret_key =
"5ji8DpnX761QAWXZwSl-2Y-mdW4yTcMimdOJ8SSxCh44wFE0jEbGBUf-VydKwnTLzBiAUedQKs3X_q1j_3lgrw" "5ji8DpnX761QAWXZwSl-2Y-mdW4yTcMimdOJ8SSxCh44wFE0jEbGBUf-VydKwnTLzBiAUedQKs3X_q1j_3lgrw"