mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-11 23:44:23 +08:00
Shows only the secrets from selected hub (#1747)
This commit is contained in:
parent
9af6fed028
commit
8754bc0d5a
21 changed files with 320 additions and 401 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()}})}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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}/)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue