Shows only the secrets from selected hub (#1747)

This commit is contained in:
Alexandre de Souza 2023-03-07 15:24:07 -03:00 committed by GitHub
parent 9af6fed028
commit 8754bc0d5a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 320 additions and 401 deletions

View file

@ -57,7 +57,7 @@ defmodule Livebook.Application do
display_startup_info()
insert_personal_hub()
Livebook.Hubs.connect_hubs()
update_app_secrets_origin()
migrate_secrets()
deploy_apps()
result
@ -191,10 +191,16 @@ defmodule Livebook.Application do
secrets =
for {"LB_" <> name = var, value} <- System.get_env() do
System.delete_env(var)
%Livebook.Secrets.Secret{name: name, value: value, origin: :startup}
%Livebook.Secrets.Secret{
name: name,
value: value,
hub_id: Livebook.Hubs.Personal.id(),
readonly: true
}
end
Livebook.Secrets.set_startup_secrets(secrets)
Livebook.Hubs.Personal.set_startup_secrets(secrets)
end
defp config_env_var?("LIVEBOOK_" <> _), do: true
@ -208,9 +214,9 @@ defmodule Livebook.Application do
end
defp insert_personal_hub do
unless Livebook.Hubs.hub_exists?("personal-hub") do
unless Livebook.Hubs.hub_exists?(Livebook.Hubs.Personal.id()) do
Livebook.Hubs.save_hub(%Livebook.Hubs.Personal{
id: "personal-hub",
id: Livebook.Hubs.Personal.id(),
hub_name: "My Hub",
hub_emoji: "🏠"
})
@ -218,10 +224,17 @@ defmodule Livebook.Application do
end
# TODO: Remove in the future
defp update_app_secrets_origin do
for %{origin: :app} = secret <- Livebook.Secrets.get_secrets() do
{:ok, secret} = Livebook.Secrets.update_secret(secret, %{origin: {:hub, "personal-hub"}})
defp migrate_secrets do
for %{name: name, value: value} <- Livebook.Storage.all(:secrets) do
secret = %Livebook.Secrets.Secret{
name: name,
value: value,
hub_id: Livebook.Hubs.Personal.id(),
readonly: false
}
Livebook.Secrets.set_secret(secret)
Livebook.Storage.delete(:secrets, name)
end
end

View file

@ -1,50 +0,0 @@
defmodule Livebook.EctoTypes.SecretOrigin do
@moduledoc false
use Ecto.Type
@type t :: nil | :session | :startup | {:hub, String.t()}
@impl true
def type, do: :string
@impl true
def load(origin), do: decode(origin)
@impl true
def dump(:session), do: {:ok, "session"}
def dump(:startup), do: {:ok, "startup"}
def dump({:hub, id}), do: {:ok, "hub-#{id}"}
def dump(_), do: :error
@impl true
def cast(:session), do: {:ok, :session}
def cast(:startup), do: {:ok, :startup}
def cast({:hub, id}), do: {:ok, {:hub, id}}
def cast(encoded) when is_binary(encoded) do
case decode(encoded) do
{:ok, origin} -> {:ok, origin}
:error -> {:error, message: "is invalid"}
end
end
def cast(_), do: {:error, message: "is invalid"}
@doc """
Encodes origin into string representation.
"""
@spec encode(t()) :: String.t()
def encode(:session), do: "session"
def encode(:startup), do: "startup"
def encode({:hub, id}), do: "hub-#{id}"
@doc """
Decodes origin from string representation.
"""
@spec decode(String.t()) :: {:ok, t()} | :error
def decode("session"), do: {:ok, :session}
def decode("startup"), do: {:ok, :startup}
def decode("hub-" <> id), do: {:ok, {:hub, id}}
def decode(_other), do: :error
end

View file

@ -205,12 +205,24 @@ defmodule Livebook.Hubs do
do: secret
end
@doc """
Gets a list of secrets for given hub.
"""
@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()
else
[]
end
end
@doc """
Creates a secret for given hub.
"""
@spec create_secret(Secret.t()) :: :ok | {:error, list({atom(), list(String.t())})}
def create_secret(%Secret{origin: {:hub, id}} = secret) do
{:ok, hub} = get_hub(id)
@spec create_secret(Provider.t(), Secret.t()) ::
:ok | {:error, list({atom(), list(String.t())})}
def create_secret(hub, %Secret{} = secret) do
true = capability?(hub, [:create_secret])
Provider.create_secret(hub, secret)
@ -219,22 +231,18 @@ defmodule Livebook.Hubs do
@doc """
Updates a secret for given hub.
"""
@spec update_secret(Secret.t()) :: :ok | {:error, list({atom(), list(String.t())})}
def update_secret(%Secret{origin: {:hub, id}} = secret) do
{:ok, hub} = get_hub(id)
true = capability?(hub, [:update_secret])
@spec update_secret(Provider.t(), Secret.t()) ::
:ok | {:error, list({atom(), list(String.t())})}
def update_secret(hub, %Secret{readonly: false} = secret) do
Provider.update_secret(hub, secret)
end
@doc """
Deletes a secret for given hub.
"""
@spec delete_secret(Secret.t()) :: :ok | {:error, list({atom(), list(String.t())})}
def delete_secret(%Secret{origin: {:hub, id}} = secret) do
{:ok, hub} = get_hub(id)
true = capability?(hub, [:delete_secret])
@spec delete_secret(Provider.t(), Secret.t()) ::
:ok | {:error, list({atom(), list(String.t())})}
def delete_secret(hub, %Secret{readonly: false} = secret) do
Provider.delete_secret(hub, secret)
end

View file

@ -171,7 +171,7 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Enterprise do
{:transport_error, reason} ->
message = "#{enterprise.hub_emoji} #{enterprise.hub_name}: #{reason}"
changeset = Livebook.Secrets.add_secret_error(secret, :origin, message)
changeset = Livebook.Secrets.add_secret_error(secret, :hub_id, message)
{:error, changeset}
end

View file

@ -154,7 +154,7 @@ defmodule Livebook.Hubs.EnterpriseClient do
end
defp build_secret(state, %{name: name, value: value}),
do: %Secret{name: name, value: value, origin: {:hub, state.hub.id}}
do: %Secret{name: name, value: value, hub_id: state.hub.id, readonly: true}
defp update_hub(state, name) do
case Enterprise.update_hub(state.hub, %{hub_name: name}) do

View file

@ -19,6 +19,12 @@ defmodule Livebook.Hubs.Personal do
@fields ~w(hub_name hub_emoji)a
@doc """
The personal hub fixed id.
"""
@spec id() :: String.t()
def id, do: "personal-hub"
@doc """
Returns an `%Ecto.Changeset{}` for tracking hub changes.
"""
@ -57,7 +63,25 @@ defmodule Livebook.Hubs.Personal do
personal
|> cast(attrs, @fields)
|> validate_required(@fields)
|> put_change(:id, "personal-hub")
|> 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
end
@ -85,10 +109,10 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Personal do
def disconnect(_personal), do: raise("not implemented")
def capabilities(_personal), do: ~w(list_secrets create_secret update_secret delete_secret)a
def capabilities(_personal), do: ~w(list_secrets create_secret)a
def get_secrets(_personal) do
Secrets.get_secrets()
def get_secrets(personal) do
Secrets.get_secrets(personal) ++ Livebook.Hubs.Personal.get_startup_secrets()
end
def create_secret(_personal, secret) do
@ -101,8 +125,8 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Personal do
:ok = Broadcasts.secret_updated(secret)
end
def delete_secret(_personal, secret) do
:ok = Secrets.unset_secret(secret.name)
def delete_secret(personal, secret) do
:ok = Secrets.unset_secret(personal, secret.name)
:ok = Broadcasts.secret_deleted(secret)
end

View file

@ -4,7 +4,7 @@ defprotocol Livebook.Hubs.Provider do
alias Livebook.Secrets.Secret
@type t :: Livebook.Hubs.Enterprise.t() | Livebook.Hubs.Fly.t() | Livebook.Hubs.Personal.t()
@type capability :: :connect | :list_secrets | :create_secret | :update_secret | :delete_secret
@type capability :: :connect | :list_secrets | :create_secret
@type capabilities :: list(capability())
@type changeset_errors :: %{required(:errors) => list({String.t(), {Stirng.t(), list()}})}

View file

@ -1,50 +1,46 @@
defmodule Livebook.Secrets do
@moduledoc false
alias Livebook.Hubs.Provider
alias Livebook.Storage
alias Livebook.Secrets.Secret
@secret_startup_key :livebook_startup_secrets
@namespace :hub_secrets
@doc """
Get the secrets list from storage.
"""
@spec get_secrets() :: list(Secret.t())
def get_secrets do
startup_secrets = :persistent_term.get(@secret_startup_key, [])
storage_secrets = for fields <- Storage.all(:secrets), do: to_struct(fields)
Enum.concat(storage_secrets, startup_secrets)
@spec get_secrets(Provider.t()) :: list(Secret.t())
def get_secrets(hub) do
for fields <- Storage.all(@namespace),
from_hub?(fields, hub),
do: to_struct(fields)
end
@doc """
Gets a secret from storage.
Raises `RuntimeError` if the secret doesn't exist.
"""
@spec fetch_secret!(String.t()) :: Secret.t()
def fetch_secret!(id) do
fields = Storage.fetch!(:secrets, id)
@spec fetch_secret!(Provider.t(), String.t()) :: Secret.t()
def fetch_secret!(hub, id) do
fields = Storage.fetch!(@namespace, id)
true = from_hub?(fields, hub)
to_struct(fields)
end
@doc """
Gets a secret from storage.
"""
@spec get_secret(String.t()) :: {:ok, Secret.t()} | :error
def get_secret(id) do
with {:ok, fields} <- Storage.fetch(:secrets, id) do
{:ok, to_struct(fields)}
@spec get_secret(Provider.t(), String.t()) :: {:ok, Secret.t()} | :error
def get_secret(hub, id) do
with {:ok, fields} <- Storage.fetch(@namespace, id) do
if from_hub?(fields, hub),
do: {:ok, to_struct(fields)},
else: :error
end
end
@doc """
Checks if the secret already exists.
"""
@spec secret_exists?(String.t()) :: boolean()
def secret_exists?(id) do
Storage.fetch(:secrets, id) != :error
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking secret changes.
"""
@ -83,63 +79,39 @@ defmodule Livebook.Secrets do
"""
@spec set_secret(Secret.t()) :: Secret.t()
def set_secret(secret) do
attributes = Map.from_struct(secret)
attributes =
secret
|> Map.from_struct()
|> Map.delete(:readonly)
:ok = Storage.insert(:secrets, secret.name, Map.to_list(attributes))
:ok = broadcast_secrets_change({:set_secret, secret})
:ok = Storage.insert(@namespace, secret.name, Map.to_list(attributes))
to_struct(attributes)
secret
end
@doc """
Unset secret from given id.
"""
@spec unset_secret(String.t()) :: :ok
def unset_secret(id) do
with {:ok, secret} <- get_secret(id) do
Storage.delete(:secrets, id)
broadcast_secrets_change({:unset_secret, secret})
@spec unset_secret(Provider.t(), String.t()) :: :ok
def unset_secret(hub, id) do
with {:ok, _secret} <- get_secret(hub, id) do
Storage.delete(@namespace, id)
end
:ok
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 """
Subscribe to secrets updates.
## Messages
* `{:set_secret, secret}`
* `{:unset_secret, secret}`
"""
@spec subscribe() :: :ok | {:error, term()}
def subscribe do
Phoenix.PubSub.subscribe(Livebook.PubSub, "secrets")
end
@doc """
Unsubscribes from `subscribe/0`.
"""
@spec unsubscribe() :: :ok
def unsubscribe do
Phoenix.PubSub.unsubscribe(Livebook.PubSub, "secrets")
end
defp broadcast_secrets_change(message) do
Phoenix.PubSub.broadcast(Livebook.PubSub, "secrets", message)
end
defp to_struct(%{name: name, value: value} = fields) do
# Previously stored secrets were all `:app`-based secrets
%Secret{name: name, value: value, origin: fields[:origin] || :app}
%Secret{
name: name,
value: value,
hub_id: fields[:hub_id] || Livebook.Hubs.Personal.id(),
readonly: false
}
end
defp from_hub?(fields, hub) do
hub_id = fields[:hub_id] || Livebook.Hubs.Personal.id()
hub_id == hub.id
end
end

View file

@ -3,27 +3,27 @@ defmodule Livebook.Secrets.Secret do
use Ecto.Schema
import Ecto.Changeset
alias Livebook.EctoTypes.SecretOrigin
@type t :: %__MODULE__{
name: String.t() | nil,
value: String.t() | nil,
origin: SecretOrigin.t()
hub_id: String.t() | nil,
readonly: boolean()
}
@primary_key {:name, :string, autogenerate: false}
embedded_schema do
field :value, :string
field :origin, SecretOrigin
field :hub_id, :string
field :readonly, :boolean, virtual: true, default: false
end
def changeset(secret, attrs \\ %{}) do
secret
|> cast(attrs, [:name, :value, :origin])
|> cast(attrs, [:name, :value, :hub_id])
|> update_change(:name, &String.upcase/1)
|> validate_format(:name, ~r/^\w+$/,
message: "should contain only alphanumeric characters and underscore"
)
|> validate_required([:name, :value, :origin])
|> validate_required([:name, :value, :hub_id])
end
end

View file

@ -278,7 +278,7 @@ defmodule Livebook.Session.Data do
mode: opts[:mode],
apps: [],
app_data: app_data,
hub: Livebook.Hubs.fetch_hub!("personal-hub")
hub: Livebook.Hubs.fetch_hub!(Livebook.Hubs.Personal.id())
}
data

View file

@ -24,7 +24,7 @@ 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_changed)a
@connection_events ~w(hub_connected hub_disconnected hub_changed)a
defp handle_info(event, socket) when event in @connection_events do
{:cont, assign(socket, saved_hubs: Livebook.Hubs.get_metadatas())}

View file

@ -5,7 +5,7 @@ defmodule LivebookWeb.SessionLive do
import LivebookWeb.SessionHelpers
import Livebook.Utils, only: [format_bytes: 1]
alias Livebook.{Sessions, Session, Delta, Notebook, Runtime, LiveMarkdown, Secrets}
alias Livebook.{Sessions, Session, Delta, Notebook, Runtime, LiveMarkdown}
alias Livebook.Notebook.{Cell, ContentLoader}
alias Livebook.JSInterop
alias Livebook.Hubs
@ -24,7 +24,6 @@ defmodule LivebookWeb.SessionLive do
Session.register_client(session_pid, self(), socket.assigns.current_user)
Session.subscribe(session_id)
Secrets.subscribe()
Hubs.subscribe(:secrets)
Livebook.NotebookManager.subscribe_starred_notebooks()
@ -61,7 +60,7 @@ defmodule LivebookWeb.SessionLive do
data_view: data_to_view(data),
autofocus_cell_id: autofocus_cell_id(data.notebook),
page_title: get_page_title(data.notebook.name),
saved_secrets: get_saved_secrets(),
saved_secrets: Hubs.get_secrets(data.hub),
select_secret_ref: nil,
select_secret_options: nil,
allowed_uri_schemes: Livebook.Config.allowed_uri_schemes(),
@ -201,7 +200,7 @@ defmodule LivebookWeb.SessionLive do
id="secrets-list"
session={@session}
saved_secrets={@saved_secrets}
hubs={@saved_hubs}
hub={@data_view.notebook_hub}
secrets={@data_view.secrets}
/>
</div>
@ -521,6 +520,7 @@ defmodule LivebookWeb.SessionLive do
id="secrets"
session={@session}
secrets={@data_view.secrets}
hub={@data_view.notebook_hub}
saved_secrets={@saved_secrets}
prefill_secret_name={@prefill_secret_name}
select_secret_ref={@select_secret_ref}
@ -1223,29 +1223,29 @@ defmodule LivebookWeb.SessionLive do
{:noreply, handle_operation(socket, operation)}
end
def handle_info({:secret_created, %{origin: {:hub, _id}}}, socket) do
def handle_info({:secret_created, _secret}, socket) do
{:noreply,
socket
|> assign(saved_secrets: get_saved_secrets())
|> refresh_secrets()
|> put_flash(:info, "A new secret has been created on your Livebook Hub")}
end
def handle_info({:secret_updated, %{origin: {:hub, _id}}}, socket) do
def handle_info({:secret_updated, _secret}, socket) do
{:noreply,
socket
|> assign(saved_secrets: get_saved_secrets())
|> refresh_secrets()
|> put_flash(:info, "An existing secret has been updated on your Livebook Hub")}
end
def handle_info({:secret_deleted, %{origin: {:hub, _id}}}, socket) do
def handle_info({:secret_deleted, _secret}, socket) do
{:noreply,
socket
|> assign(saved_secrets: get_saved_secrets())
|> refresh_secrets()
|> put_flash(:info, "An existing secret has been deleted on your Livebook Hub")}
end
def handle_info(:hubs_changed, socket) do
{:noreply, assign(socket, saved_secrets: get_saved_secrets())}
def handle_info(:hub_changed, socket) do
{:noreply, refresh_secrets(socket)}
end
def handle_info({:error, error}, socket) do
@ -1330,26 +1330,6 @@ defmodule LivebookWeb.SessionLive do
handle_event("insert_cell_below", params, socket)
end
def handle_info({:set_secret, secret}, socket) do
saved_secrets =
Enum.reject(
socket.assigns.saved_secrets,
&(&1.name == secret.name and &1.origin == secret.origin)
)
{:noreply, assign(socket, saved_secrets: [secret | saved_secrets])}
end
def handle_info({:unset_secret, secret}, socket) do
saved_secrets =
Enum.reject(
socket.assigns.saved_secrets,
&(&1.name == secret.name and &1.origin == secret.origin)
)
{:noreply, assign(socket, saved_secrets: saved_secrets)}
end
def handle_info({:push_patch, to}, socket) do
{:noreply, push_patch(socket, to: to)}
end
@ -1656,6 +1636,9 @@ defmodule LivebookWeb.SessionLive do
prune_cell_sources(socket)
end
defp after_operation(socket, _prev_socket, {:set_notebook_hub, _client_id, _id}),
do: refresh_secrets(socket)
defp after_operation(socket, _prev_socket, _operation), do: socket
defp handle_actions(socket, actions) do
@ -2098,14 +2081,13 @@ defmodule LivebookWeb.SessionLive do
end)
end
defp get_saved_secrets do
Enum.sort(Hubs.get_secrets())
end
defp app_status_color(nil), do: "bg-gray-400"
defp app_status_color(:booting), do: "bg-blue-500"
defp app_status_color(:running), do: "bg-green-bright-400"
defp app_status_color(:error), do: "bg-red-400"
defp app_status_color(:shutting_down), do: "bg-gray-500"
defp app_status_color(:stopped), do: "bg-gray-500"
defp refresh_secrets(socket),
do: assign(socket, saved_secrets: Hubs.get_secrets(socket.private.data.hub))
end

View file

@ -5,11 +5,10 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
alias Livebook.Secrets
alias Livebook.Secrets.Secret
alias Livebook.Session
alias Livebook.EctoTypes.SecretOrigin
@impl true
def mount(socket) do
{:ok, assign(socket, title: title(socket), hubs: Livebook.Hubs.get_hubs([:create_secret]))}
{:ok, assign(socket, title: title(socket))}
end
@impl true
@ -21,11 +20,14 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
socket =
socket
|> assign_new(:changeset, fn ->
attrs = %{name: secret_name, value: nil, origin: :session}
attrs = %{name: secret_name, value: nil, hub_id: "session", readonly: false}
Secrets.change_secret(%Secret{}, attrs)
end)
|> assign_new(:grant_access_secret, fn ->
Enum.find(socket.assigns.saved_secrets, &(&1.name == secret_name))
Enum.find(
socket.assigns.saved_secrets,
&(&1.name == secret_name and secret_name not in Map.keys(socket.assigns.secrets))
)
end)
{:ok, socket}
@ -42,6 +44,7 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
:if={@grant_access_secret}
secret={@grant_access_secret}
target={@myself}
hub={@hub}
/>
<div class="flex flex-columns gap-4">
<div :if={@select_secret_ref} class="basis-1/2 grow-0 pr-4 border-r">
@ -60,9 +63,9 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
/>
<.secret_with_badge
:for={secret <- @saved_secrets}
:if={!is_map_key(@secrets, secret.name) and @secrets[secret.name] != secret.value}
secret_name={secret.name}
secret_origin={secret.origin}
stored={stored(secret, @hubs)}
stored={hub_label(@hub)}
active={false}
target={@myself}
/>
@ -109,17 +112,12 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
phx-debounce="blur"
/>
<.radio_field
field={f[:origin]}
value={SecretOrigin.encode(f[:origin].value)}
field={f[:hub_id]}
label="Storage"
options={
[{"session", "only this session"}] ++
if Livebook.Config.feature_flag_enabled?(:hub) do
for hub <- @hubs, do: {"hub-#{hub.id}", "in #{hub.hub_emoji} #{hub.hub_name}"}
else
[]
end
}
options={[
{"session", "only this session"},
{@hub.id, "in #{@hub.hub_emoji} #{@hub.hub_name}"}
]}
/>
<div class="flex space-x-2">
<button class="button-base button-blue" type="submit" disabled={not @changeset.valid?}>
@ -138,6 +136,8 @@ 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"
@ -150,9 +150,9 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
end
]}
phx-value-name={@secret_name}
phx-value-origin={SecretOrigin.encode(@secret_origin)}
phx-value-origin={@secret_origin}
phx-target={@target}
phx-click="select_secret"
phx-click="grant_access"
>
<%= @secret_name %>
<span class={[
@ -188,25 +188,17 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
icon="error-warning-fill"
class="align-middle text-2xl flex text-gray-100 rounded-lg py-2"
/>
<%= if @secret.origin == :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 %>
<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 <%= hub_label(@hub) %>. Allow this session to access it?
</span>
</div>
<button
class="button-base button-gray"
phx-click="grant_access"
phx-value-name={@secret.name}
phx-value-origin={SecretOrigin.encode(@secret.origin)}
phx-value-hub_id={@secret.hub_id}
phx-target={@target}
>
Grant access
@ -232,16 +224,6 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
end
end
def handle_event("select_secret", %{"name" => secret_name, "origin" => origin}, socket) do
{:ok, origin} = SecretOrigin.decode(origin)
grant_access(socket.assigns.saved_secrets, secret_name, origin, socket)
{:noreply,
socket
|> push_patch(to: socket.assigns.return_to)
|> push_secret_selected(secret_name)}
end
def handle_event("validate", %{"secret" => attrs}, socket) do
changeset =
%Secret{}
@ -251,9 +233,17 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
{:noreply, assign(socket, changeset: changeset)}
end
def handle_event("grant_access", %{"name" => secret_name, "origin" => origin}, socket) do
{:ok, origin} = SecretOrigin.decode(origin)
grant_access(socket.assigns.saved_secrets, secret_name, origin, socket)
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]
})
secret = Enum.find(socket.assigns.saved_secrets, &(&1.name == secret_name)) ->
Session.set_secret(socket.assigns.session.pid, secret)
end
{:noreply,
socket
@ -271,31 +261,15 @@ 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{origin: :session} = secret) do
defp set_secret(socket, %Secret{hub_id: "session"} = secret) do
Session.set_secret(socket.assigns.session.pid, secret)
end
defp set_secret(socket, %Secret{origin: {:hub, id}} = secret) when is_binary(id) do
with :ok <- Hubs.create_secret(secret) do
defp set_secret(socket, %Secret{} = secret) do
with :ok <- Hubs.create_secret(socket.assigns.hub, secret) do
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: Livebook.Session.set_secret(socket.assigns.session.pid, secret)
end
defp stored(%{origin: {:hub, id}}, hubs), do: stored(id, hubs)
defp stored(%{origin: :startup}, hubs), do: stored("personal-hub", hubs)
defp stored(id, hubs) do
hub = fetch_hub!(id, hubs)
"#{hub.hub_emoji} #{hub.hub_name}"
end
defp fetch_hub!(id, hubs) do
Enum.find(hubs, &(&1.id == id)) || raise "unknown hub id: #{id}"
end
defp hub_label(hub), do: "#{hub.hub_emoji} #{hub.hub_name}"
end

View file

@ -1,6 +1,11 @@
defmodule LivebookWeb.SessionLive.SecretsListComponent do
use LivebookWeb, :live_component
alias Livebook.Hubs
alias Livebook.Session
alias Livebook.Secrets
alias Livebook.Secrets.Secret
@impl true
def render(assigns) do
~H"""
@ -69,7 +74,7 @@ defmodule LivebookWeb.SessionLive.SecretsListComponent do
<div :if={Livebook.Config.feature_flag_enabled?(:hub)} class="mt-16">
<h3 class="uppercase text-sm font-semibold text-gray-500">
Hub secrets
<%= @hub.hub_emoji %> <%= @hub.hub_name %> secrets
</h3>
<span class="text-sm text-gray-500">
<%= if @saved_secrets == [] do %>
@ -84,9 +89,9 @@ defmodule LivebookWeb.SessionLive.SecretsListComponent do
<.secrets_item
:for={secret <- @saved_secrets}
secret={secret}
prefix={prefix(secret)}
prefix={"hub-#{secret.hub_id}"}
data_secrets={@secrets}
hubs={@hubs}
hub={@hub}
myself={@myself}
/>
</div>
@ -121,11 +126,7 @@ defmodule LivebookWeb.SessionLive.SecretsListComponent do
phx-change="toggle_secret"
phx-target={@myself}
>
<.switch_field
field={f[:toggled]}
label={secret_label(@secret, @hubs)}
tooltip={secret_tooltip(@secret, @hubs)}
/>
<.switch_field field={f[:toggled]} />
<.hidden_field field={f[:name]} value={@secret.name} />
<.hidden_field field={f[:value]} value={@secret.value} />
</.form>
@ -138,14 +139,21 @@ defmodule LivebookWeb.SessionLive.SecretsListComponent do
<%= @secret.value %>
</span>
<button
:if={delete?(@secret, @hubs)}
:if={!@secret.readonly}
id={"#{@prefix}-secret-#{@secret.name}-delete"}
type="button"
phx-click={
with_confirm(
JS.push("delete_app_secret", value: %{secret_name: @secret.name}, target: @myself),
title: "Delete app secret - #{@secret.name}",
description: "Are you sure you want to delete this app secret?",
JS.push("delete_hub_secret",
value: %{
name: @secret.name,
value: @secret.value,
hub_id: @secret.hub_id
},
target: @myself
),
title: "Delete hub secret - #{@secret.name}",
description: "Are you sure you want to delete this hub secret?",
confirm_text: "Delete",
confirm_icon: "delete-bin-6-line"
)
@ -183,45 +191,28 @@ defmodule LivebookWeb.SessionLive.SecretsListComponent do
def handle_event("toggle_secret", %{"data" => data}, socket) do
if data["toggled"] == "true" do
secret = %{name: data["name"], value: data["value"]}
Livebook.Session.set_secret(socket.assigns.session.pid, secret)
Session.set_secret(socket.assigns.session.pid, secret)
else
Livebook.Session.unset_secret(socket.assigns.session.pid, data["name"])
Session.unset_secret(socket.assigns.session.pid, data["name"])
end
{:noreply, socket}
end
def handle_event("delete_session_secret", %{"secret_name" => secret_name}, socket) do
Livebook.Session.unset_secret(socket.assigns.session.pid, secret_name)
Session.unset_secret(socket.assigns.session.pid, secret_name)
{:noreply, socket}
end
def handle_event("delete_app_secret", %{"secret_name" => secret_name}, socket) do
Livebook.Secrets.unset_secret(secret_name)
def handle_event("delete_hub_secret", attrs, socket) do
{:ok, secret} = Secrets.update_secret(%Secret{}, attrs)
:ok = Hubs.delete_secret(socket.assigns.hub, secret)
:ok = Session.unset_secret(socket.assigns.session.pid, secret.name)
{:noreply, socket}
end
defp secret_toggled?(secret, secrets) do
Map.has_key?(secrets, secret.name) and secrets[secret.name] == secret.value
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
defp prefix(%{origin: {:hub, id}}), do: "hub-#{id}"
defp prefix(%{origin: :startup}), do: "hub-personal-hub"
defp delete?(%{origin: {:hub, id}}, hubs) do
hub = fetch_hub!(id, hubs)
Livebook.Hubs.capability?(hub.provider, [:delete_secret])
end
defp delete?(_, _), do: false
end

View file

@ -55,7 +55,7 @@ defmodule Livebook.Hubs.EnterpriseClientTest do
name = "API_TOKEN_ID"
value = Livebook.Utils.random_id()
:erpc.call(node, Enterprise.Integration, :create_secret, [name, value])
secret = %Secret{name: name, value: value, origin: {:hub, id}}
secret = %Secret{name: name, value: value, hub_id: id, readonly: true}
assert_receive {:secret_created, ^secret}
assert secret in EnterpriseClient.get_secrets(id)
@ -66,8 +66,8 @@ defmodule Livebook.Hubs.EnterpriseClientTest do
value = "JakePeralta"
new_value = "ChonkyCat"
enterprise_secret = :erpc.call(node, Enterprise.Integration, :create_secret, [name, value])
secret = %Secret{name: name, value: value, origin: {:hub, id}}
updated_secret = %Secret{name: name, value: new_value, origin: {:hub, id}}
secret = %Secret{name: name, value: value, hub_id: id, readonly: true}
updated_secret = %Secret{name: name, value: new_value, hub_id: id, readonly: true}
assert_receive {:secret_created, ^secret}
assert secret in EnterpriseClient.get_secrets(id)
@ -85,7 +85,7 @@ defmodule Livebook.Hubs.EnterpriseClientTest do
name = "SUPER_DELETE"
value = "JakePeralta"
enteprise_secret = :erpc.call(node, Enterprise.Integration, :create_secret, [name, value])
secret = %Secret{name: name, value: value, origin: {:hub, id}}
secret = %Secret{name: name, value: value, hub_id: id, readonly: true}
assert_receive {:secret_created, ^secret}
assert secret in EnterpriseClient.get_secrets(id)

View file

@ -26,48 +26,45 @@ defmodule Livebook.Hubs.ProviderTest do
end
test "capabilities/1", %{hub: hub} do
assert Provider.capabilities(hub) == [
:list_secrets,
:create_secret,
:update_secret,
:delete_secret
]
assert Provider.capabilities(hub) == [:list_secrets, :create_secret]
end
test "get_secrets/1 without startup secrets", %{hub: hub} do
secret = insert_secret(name: "GET_PERSONAL_SECRET", origin: {:hub, "personal-hub"})
secret = insert_secret(name: "GET_PERSONAL_SECRET")
assert secret in Provider.get_secrets(hub)
end
test "get_secrets/1 with startup secrets", %{hub: hub} do
secret = build(:secret, name: "GET_PERSONAL_SECRET", origin: :startup)
Livebook.Secrets.set_startup_secrets([secret])
secret = build(:secret, name: "GET_PERSONAL_SECRET", readonly: true)
Livebook.Hubs.Personal.set_startup_secrets([secret])
assert secret in Provider.get_secrets(hub)
end
test "create_secret/1", %{hub: hub} do
secret = build(:secret, name: "CREATE_PERSONAL_SECRET", origin: {:hub, "personal-hub"})
secret = build(:secret, name: "CREATE_PERSONAL_SECRET")
assert Provider.create_secret(hub, secret) == :ok
assert secret in Provider.get_secrets(hub)
end
test "update_secret/1", %{hub: hub} do
secret = insert_secret(name: "UPDATE_PERSONAL_SECRET", origin: {:hub, "personal-hub"})
assert secret in Secrets.get_secrets()
secret = insert_secret(name: "UPDATE_PERSONAL_SECRET")
assert secret in Secrets.get_secrets(hub)
updated_secret = %{secret | value: "123321"}
assert Provider.update_secret(hub, updated_secret) == :ok
assert updated_secret in Secrets.get_secrets()
refute secret in Secrets.get_secrets()
assert updated_secret in Secrets.get_secrets(hub)
refute secret in Secrets.get_secrets(hub)
end
test "delete_secret/1", %{hub: hub} do
secret = insert_secret(name: "DELETE_PERSONAL_SECRET", origin: {:hub, "personal-hub"})
assert secret in Secrets.get_secrets()
secret = insert_secret(name: "DELETE_PERSONAL_SECRET")
assert secret in Secrets.get_secrets(hub)
assert Provider.delete_secret(hub, secret) == :ok
refute secret in Secrets.get_secrets()
refute secret in Secrets.get_secrets(hub)
end
test "connection_error/1", %{hub: hub} do

View file

@ -5,51 +5,31 @@ defmodule Livebook.SecretsTest do
alias Livebook.Secrets
alias Livebook.Secrets.Secret
describe "get_secrets/0" do
test "returns a list of secrets from storage" do
secret = build(:secret, name: "FOO", value: "111")
Secrets.set_secret(secret)
assert secret in Secrets.get_secrets()
Secrets.unset_secret(secret.name)
refute secret in Secrets.get_secrets()
end
test "returns a list of secrets from temporary storage" do
secret = build(:secret, name: "FOO", value: "222", origin: :startup)
Secrets.set_startup_secrets([secret])
assert secret in Secrets.get_secrets()
# We can't delete from temporary storage, since it will be deleted
# on next startup, if not provided
Secrets.unset_secret(secret.name)
assert secret in Secrets.get_secrets()
end
setup do
{:ok, hub: build(:personal)}
end
test "fetch an specific secret" do
test "get_secrets/1 returns a list of secrets from storage", %{hub: hub} do
secret = build(:secret, name: "FOO", value: "111")
Secrets.set_secret(secret)
assert secret in Secrets.get_secrets(hub)
Secrets.unset_secret(hub, secret.name)
refute secret in Secrets.get_secrets(hub)
end
test "fetch an specific secret", %{hub: hub} do
secret = insert_secret(name: "FOO", value: "111")
assert_raise Livebook.Storage.NotFoundError,
~s(could not find entry in \"secrets\" with ID "NOT_HERE"),
~s(could not find entry in "hub_secrets" with ID "NOT_HERE"),
fn ->
Secrets.fetch_secret!("NOT_HERE")
Secrets.fetch_secret!(hub, "NOT_HERE")
end
assert Secrets.fetch_secret!(secret.name) == secret
Secrets.unset_secret(secret.name)
end
test "secret_exists?/1" do
Secrets.unset_secret("FOO")
refute Secrets.secret_exists?("FOO")
insert_secret(name: "FOO", value: "111")
assert Secrets.secret_exists?("FOO")
Secrets.unset_secret("FOO")
assert Secrets.fetch_secret!(hub, secret.name) == secret
Secrets.unset_secret(hub, secret.name)
end
describe "update_secret/2" do
@ -59,7 +39,8 @@ defmodule Livebook.SecretsTest do
assert {:ok, secret} = Secrets.update_secret(%Secret{}, attrs)
assert attrs.name == secret.name
assert attrs.value == secret.value
assert attrs.origin == secret.origin
assert attrs.hub_id == secret.hub_id
refute secret.readonly
end
test "returns changeset error" do

View file

@ -30,6 +30,7 @@ defmodule LivebookWeb.SessionLive.SecretsComponentTest do
)
{:ok, session} = Sessions.create_session(notebook: Livebook.Notebook.new())
Session.set_notebook_hub(session.pid, hub_id)
on_exit(fn ->
Livebook.Hubs.delete_hub(hub_id)
@ -43,14 +44,17 @@ defmodule LivebookWeb.SessionLive.SecretsComponentTest 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: {:hub, id})
secret =
build(:secret, name: "BIG_IMPORTANT_SECRET", value: "123", hub_id: id, readonly: true)
{:ok, view, _html} = live(conn, ~p"/sessions/#{session.id}/secrets")
attrs = %{
secret: %{
name: secret.name,
value: secret.value,
origin: "hub-#{enterprise.id}"
hub_id: enterprise.id
}
}
@ -61,7 +65,6 @@ defmodule LivebookWeb.SessionLive.SecretsComponentTest do
assert_receive {:secret_created, ^secret}
assert render(view) =~ "A new secret has been created on your Livebook Hub"
assert has_element?(view, "#hub-#{enterprise.id}-secret-#{secret.name}-wrapper")
assert has_element?(view, ~s/[data-tooltip="#{enterprise.hub_name}"]/, enterprise.hub_emoji)
end
test "toggle a secret from Enterprise hub",
@ -70,7 +73,8 @@ defmodule LivebookWeb.SessionLive.SecretsComponentTest do
build(:secret,
name: "POSTGRES_PASSWORD",
value: "postgres",
origin: {:hub, enterprise.id}
hub_id: enterprise.id,
readonly: true
)
{:ok, view, _html} = live(conn, ~p"/sessions/#{session.id}")
@ -88,7 +92,8 @@ defmodule LivebookWeb.SessionLive.SecretsComponentTest do
build(:secret,
name: "PGPASS",
value: "postgres",
origin: {:hub, enterprise.id}
hub_id: enterprise.id,
readonly: true
)
# Subscribe and executes the code to trigger
@ -113,7 +118,7 @@ defmodule LivebookWeb.SessionLive.SecretsComponentTest do
secrets_component = with_target(view, "#secrets-modal")
form_element = element(secrets_component, "form[phx-submit='save']")
assert has_element?(form_element)
attrs = %{value: secret.value, origin: "hub-#{enterprise.id}"}
attrs = %{value: secret.value, hub_id: enterprise.id}
render_submit(form_element, %{secret: attrs})
# Checks we received the secret created event from Enterprise
@ -140,7 +145,8 @@ defmodule LivebookWeb.SessionLive.SecretsComponentTest do
build(:secret,
name: "MYSQL_PASS",
value: "admin",
origin: {:hub, enterprise.id}
hub_id: enterprise.id,
readonly: true
)
# Subscribe and executes the code to trigger
@ -174,7 +180,8 @@ defmodule LivebookWeb.SessionLive.SecretsComponentTest do
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?"
assert render(secrets_component) =~
"in #{hub_label(enterprise)}. Allow this session to access it?"
grant_access_button = element(secrets_component, "button", "Grant access")
render_click(grant_access_button)
@ -194,7 +201,14 @@ defmodule LivebookWeb.SessionLive.SecretsComponentTest do
test "shows secret events from Enterprise hub",
%{conn: conn, session: session, enterprise: enterprise, node: node} do
{:ok, view, _html} = live(conn, ~p"/sessions/#{session.id}/secrets")
secret = build(:secret, name: "EVENT_SECRET", value: "123", origin: {:hub, enterprise.id})
secret =
build(:secret,
name: "EVENT_SECRET",
value: "123",
hub_id: enterprise.id,
readonly: true
)
# We need the `Secret` schema from enterprise to execute
# the following functions inside `Enterprise.Integration`

View file

@ -1069,31 +1069,35 @@ defmodule LivebookWeb.SessionLiveTest do
end
describe "secrets" do
setup do
{:ok, hub: build(:personal)}
end
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", origin: :session)
secret = build(:secret, name: "FOO", value: "123", hub_id: "session")
view
|> element(~s{form[phx-submit="save"]})
|> render_submit(%{secret: %{name: secret.name, value: secret.value, origin: "session"}})
|> render_submit(%{secret: %{name: secret.name, value: secret.value, hub_id: secret.hub_id}})
assert_session_secret(view, session.pid, secret)
end
test "adds a livebook secret from form", %{conn: conn, session: session} do
test "adds a livebook secret from form", %{conn: conn, session: session, hub: hub} do
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}/secrets")
secret = build(:secret, name: "BAR", value: "456")
view
|> element(~s{form[phx-submit="save"]})
|> render_submit(%{
secret: %{name: secret.name, value: secret.value, origin: "hub-personal-hub"}
secret: %{name: secret.name, value: secret.value, hub_id: secret.hub_id}
})
assert secret in Livebook.Secrets.get_secrets()
assert secret in Livebook.Hubs.get_secrets(hub)
end
test "syncs secrets", %{conn: conn, session: session} do
test "syncs secrets", %{conn: conn, session: session, hub: hub} do
session_secret = insert_secret(name: "FOO", value: "123")
secret = build(:secret, name: "FOO", value: "456")
@ -1102,11 +1106,11 @@ defmodule LivebookWeb.SessionLiveTest do
view
|> element(~s{form[phx-submit="save"]})
|> render_submit(%{
secret: %{name: secret.name, value: secret.value, origin: "hub-personal-hub"}
secret: %{name: secret.name, value: secret.value, hub_id: secret.hub_id}
})
assert_session_secret(view, session.pid, secret)
assert secret in Livebook.Secrets.get_secrets()
assert secret in Livebook.Hubs.get_secrets(hub)
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}/secrets")
Session.set_secret(session.pid, session_secret)
@ -1116,31 +1120,32 @@ defmodule LivebookWeb.SessionLiveTest do
view
|> element(~s{form[phx-submit="save"]})
|> render_submit(%{
secret: %{name: secret.name, value: secret.value, origin: "hub-personal-hub"}
secret: %{name: secret.name, value: secret.value, hub_id: secret.hub_id}
})
assert_session_secret(view, session.pid, secret)
assert secret in Livebook.Secrets.get_secrets()
assert secret in Livebook.Hubs.get_secrets(hub)
end
test "never syncs secrets when updating from session", %{conn: conn, session: session} do
app_secret = insert_secret(name: "FOO", value: "123")
secret = build(:secret, name: "FOO", value: "456", origin: :session)
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")
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}/secrets")
Session.set_secret(session.pid, app_secret)
Session.set_secret(session.pid, hub_secret)
view
|> element(~s{form[phx-submit="save"]})
|> render_submit(%{secret: %{name: secret.name, value: secret.value, origin: "session"}})
|> render_submit(%{secret: %{name: secret.name, value: secret.value, hub_id: secret.hub_id}})
assert_session_secret(view, session.pid, secret)
refute secret in Livebook.Secrets.get_secrets()
assert app_secret in Livebook.Secrets.get_secrets()
refute secret in Livebook.Hubs.get_secrets(hub)
assert hub_secret in Livebook.Hubs.get_secrets(hub)
end
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)
secret = build(:secret, name: "ANOTHER_GREAT_SECRET", value: "123456", hub_id: "session")
Session.subscribe(session.id)
section_id = insert_section(session.pid)
code = ~s{System.fetch_env!("LB_#{secret.name}")}
@ -1157,8 +1162,8 @@ defmodule LivebookWeb.SessionLiveTest do
end
test "adding a missing secret using 'Add secret' button",
%{conn: conn, session: session} do
secret = build(:secret, name: "MYUNAVAILABLESECRET", value: "123456", origin: :session)
%{conn: conn, session: session, hub: hub} do
secret = build(:secret, name: "MYUNAVAILABLESECRET", value: "123456", hub_id: "session")
# Subscribe and executes the code to trigger
# the `System.EnvError` exception and outputs the 'Add secret' button
@ -1182,10 +1187,10 @@ defmodule LivebookWeb.SessionLiveTest do
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, %{secret: %{value: secret.value, origin: "session"}})
render_submit(form_element, %{secret: %{value: secret.value, hub_id: secret.hub_id}})
# Checks if the secret isn't an app secret
refute secret in Livebook.Secrets.get_secrets()
refute secret in Livebook.Hubs.get_secrets(hub)
# Checks if the secret exists and is inside the session,
# then executes the code cell again and checks if the
@ -1200,7 +1205,7 @@ defmodule LivebookWeb.SessionLiveTest do
end
test "granting access for unavailable secret using 'Add secret' button",
%{conn: conn, session: session} do
%{conn: conn, session: session, hub: hub} do
secret = insert_secret(name: "UNAVAILABLESECRET", value: "123456")
# Subscribe and executes the code to trigger
@ -1220,7 +1225,7 @@ defmodule LivebookWeb.SessionLiveTest do
assert has_element?(add_secret_button)
# Checks if the secret is persisted
assert secret in Livebook.Secrets.get_secrets()
assert secret in Livebook.Hubs.get_secrets(hub)
# Clicks the button and checks if the 'Grant access' banner
# is being shown, so clicks it's button to set the app secret
@ -1228,7 +1233,8 @@ defmodule LivebookWeb.SessionLiveTest do
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?"
assert render(secrets_component) =~
"in #{hub_label(secret)}. Allow this session to access it?"
grant_access_button = element(secrets_component, "button", "Grant access")
render_click(grant_access_button)
@ -1246,8 +1252,8 @@ defmodule LivebookWeb.SessionLiveTest do
end
test "loads secret from temporary storage", %{conn: conn, session: session} do
secret = build(:secret, name: "FOOBARBAZ", value: "ChonkyCat", origin: :startup)
Livebook.Secrets.set_startup_secrets([secret])
secret = build(:secret, name: "FOOBARBAZ", value: "ChonkyCat", readonly: true)
Livebook.Hubs.Personal.set_startup_secrets([secret])
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
@ -1274,9 +1280,9 @@ defmodule LivebookWeb.SessionLiveTest do
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_startup_secrets([secret])
%{conn: conn, session: session, hub: hub} do
secret = build(:secret, name: "MYSTARTUPSECRET", value: "ChonkyCat", readonly: true)
Livebook.Hubs.Personal.set_startup_secrets([secret])
# Subscribe and executes the code to trigger
# the `System.EnvError` exception and outputs the 'Add secret' button
@ -1295,7 +1301,7 @@ defmodule LivebookWeb.SessionLiveTest do
assert has_element?(add_secret_button)
# Checks if the secret is persisted
assert secret in Livebook.Secrets.get_secrets()
assert secret in Livebook.Hubs.get_secrets(hub)
# Clicks the button and checks if the 'Grant access' banner
# is being shown, so clicks it's button to set the app secret
@ -1303,7 +1309,8 @@ defmodule LivebookWeb.SessionLiveTest do
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?"
assert render(secrets_component) =~
"in #{hub_label(secret)}. Allow this session to access it?"
grant_access_button = element(secrets_component, "button", "Grant access")
render_click(grant_access_button)
@ -1477,11 +1484,12 @@ defmodule LivebookWeb.SessionLiveTest do
test "selects the notebook hub", %{conn: conn, session: session} do
hub = insert_hub(:fly)
id = hub.id
personal_id = Livebook.Hubs.Personal.id()
Session.subscribe(session.id)
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
assert %Livebook.Hubs.Personal{id: "personal-hub"} = Session.get_data(session.pid).hub
assert %Livebook.Hubs.Personal{id: ^personal_id} = Session.get_data(session.pid).hub
view
|> element(~s/#select-hub-#{id}/)

View file

@ -49,7 +49,7 @@ defmodule Livebook.Factory do
def build(:personal) do
%Livebook.Hubs.Personal{
id: "personal-hub",
id: Livebook.Hubs.Personal.id(),
hub_name: "My Hub",
hub_emoji: "🏠"
}
@ -66,7 +66,8 @@ defmodule Livebook.Factory do
%Livebook.Secrets.Secret{
name: "FOO",
value: "123",
origin: {:hub, "personal-hub"}
hub_id: Livebook.Hubs.Personal.id(),
readonly: false
}
end
@ -86,7 +87,8 @@ defmodule Livebook.Factory do
def insert_secret(attrs \\ %{}) do
secret = build(:secret, attrs)
:ok = Livebook.Hubs.create_secret(secret)
hub = Livebook.Hubs.fetch_hub!(secret.hub_id)
:ok = Livebook.Hubs.create_secret(hub, secret)
secret
end

View file

@ -1,7 +1,8 @@
defmodule Livebook.SessionHelpers do
@moduledoc false
alias Livebook.{Session, Sessions}
alias Livebook.{Hubs, Session, Sessions}
alias Livebook.Secrets.Secret
import ExUnit.Assertions
import Phoenix.LiveViewTest
@ -56,9 +57,8 @@ defmodule Livebook.SessionHelpers do
def assert_session_secret(view, session_pid, secret) do
selector =
case secret do
%{name: name, origin: :session} -> "#session-secret-#{name}-wrapper"
%{name: name, origin: :startup} -> "#hub-personal-hub-secret-#{name}-wrapper"
%{name: name, origin: {:hub, id}} -> "#hub-#{id}-secret-#{name}-wrapper"
%{name: name, hub_id: "session"} -> "#session-secret-#{name}-wrapper"
%{name: name, hub_id: id} -> "#hub-#{id}-secret-#{name}-wrapper"
end
assert has_element?(view, selector)
@ -67,4 +67,7 @@ defmodule Livebook.SessionHelpers do
assert Map.has_key?(secrets, secret.name)
assert secrets[secret.name] == secret.value
end
def hub_label(%Secret{hub_id: id}), do: hub_label(Hubs.fetch_hub!(id))
def hub_label(hub), do: "#{hub.hub_emoji} #{hub.hub_name}"
end