mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-11-09 21:51:42 +08:00
Implements :origin field to Secret struct (#1667)
This commit is contained in:
parent
eaa4856972
commit
d70764517f
16 changed files with 498 additions and 307 deletions
|
|
@ -187,7 +187,7 @@ 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}
|
||||
%Livebook.Secrets.Secret{name: name, value: value, origin: :startup}
|
||||
end
|
||||
|
||||
Livebook.Secrets.set_temporary_secrets(secrets)
|
||||
|
|
|
|||
45
lib/livebook/ecto_types/secret_origin.ex
Normal file
45
lib/livebook/ecto_types/secret_origin.ex
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
defmodule Livebook.EctoTypes.SecretOrigin do
|
||||
@moduledoc false
|
||||
use Ecto.Type
|
||||
|
||||
@type t :: nil | :session | :startup | :app | {:hub, String.t()}
|
||||
|
||||
def type, do: :string
|
||||
|
||||
def load("session"), do: {:ok, :session}
|
||||
def load("app"), do: {:ok, :app}
|
||||
def load("startup"), do: {:ok, :startup}
|
||||
|
||||
def load("hub-" <> id) do
|
||||
if hub_secret?(id),
|
||||
do: {:ok, {:hub, id}},
|
||||
else: :error
|
||||
end
|
||||
|
||||
def load(_), do: :error
|
||||
|
||||
def dump(:session), do: {:ok, "session"}
|
||||
def dump(:app), do: {:ok, "app"}
|
||||
def dump(:startup), do: {:ok, "startup"}
|
||||
|
||||
def dump({:hub, id}) when is_binary(id) do
|
||||
if hub_secret?(id), do: {:ok, "hub-#{id}"}, else: :error
|
||||
end
|
||||
|
||||
def dump(_), do: :error
|
||||
|
||||
def cast(:session), do: {:ok, :session}
|
||||
def cast(:app), do: {:ok, :app}
|
||||
def cast(:startup), do: {:ok, :startup}
|
||||
def cast({:hub, id}) when is_binary(id), do: cast(id)
|
||||
|
||||
def cast(id) when is_binary(id) do
|
||||
if hub_secret?(id),
|
||||
do: {:ok, {:hub, id}},
|
||||
else: {:error, message: "does not exists"}
|
||||
end
|
||||
|
||||
def cast(_), do: {:error, message: "is invalid"}
|
||||
|
||||
defdelegate hub_secret?(id), to: Livebook.Hubs, as: :hub_exists?
|
||||
end
|
||||
|
|
@ -95,14 +95,14 @@ defmodule Livebook.Hubs.EnterpriseClient do
|
|||
end
|
||||
|
||||
def handle_info({:event, :secret_created, %{name: name, value: value}}, state) do
|
||||
secret = %Secret{name: name, value: value}
|
||||
secret = %Secret{name: name, value: value, origin: {:hub, state.hub.id}}
|
||||
Broadcasts.secret_created(secret)
|
||||
|
||||
{:noreply, put_secret(state, secret)}
|
||||
end
|
||||
|
||||
def handle_info({:event, :secret_updated, %{name: name, value: value}}, state) do
|
||||
secret = %Secret{name: name, value: value}
|
||||
secret = %Secret{name: name, value: value, origin: {:hub, state.hub.id}}
|
||||
Broadcasts.secret_updated(secret)
|
||||
|
||||
{:noreply, put_secret(state, secret)}
|
||||
|
|
@ -119,7 +119,7 @@ defmodule Livebook.Hubs.EnterpriseClient do
|
|||
{:via, Registry, {@registry, id}}
|
||||
end
|
||||
|
||||
defp put_secret(state, %Secret{name: name} = secret) do
|
||||
%{state | secrets: [secret | Enum.reject(state.secrets, &(&1.name == name))]}
|
||||
defp put_secret(state, secret) do
|
||||
%{state | secrets: [secret | Enum.reject(state.secrets, &(&1.name == secret.name))]}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,15 +11,14 @@ defmodule Livebook.Secrets do
|
|||
@doc """
|
||||
Get the secrets list from storage.
|
||||
"""
|
||||
@spec fetch_secrets() :: list(Secret.t())
|
||||
def fetch_secrets do
|
||||
@spec get_secrets() :: list(Secret.t())
|
||||
def get_secrets do
|
||||
temporary_secrets = :persistent_term.get(@temporary_key, [])
|
||||
|
||||
for fields <- Storage.all(:secrets) do
|
||||
struct!(Secret, Map.delete(fields, :id))
|
||||
to_struct(fields)
|
||||
end
|
||||
|> Enum.concat(temporary_secrets)
|
||||
|> Enum.sort()
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
@ -29,7 +28,7 @@ defmodule Livebook.Secrets do
|
|||
@spec fetch_secret!(String.t()) :: Secret.t()
|
||||
def fetch_secret!(id) do
|
||||
fields = Storage.fetch!(:secrets, id)
|
||||
struct!(Secret, Map.delete(fields, :id))
|
||||
to_struct(fields)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
@ -54,10 +53,12 @@ defmodule Livebook.Secrets do
|
|||
"""
|
||||
@spec set_secret(Secret.t()) :: Secret.t()
|
||||
def set_secret(secret) do
|
||||
attributes = secret |> Map.from_struct() |> Map.to_list()
|
||||
:ok = Storage.insert(:secrets, secret.name, attributes)
|
||||
attributes = Map.from_struct(secret)
|
||||
|
||||
:ok = Storage.insert(:secrets, secret.name, Map.to_list(attributes))
|
||||
:ok = broadcast_secrets_change({:set_secret, secret})
|
||||
secret
|
||||
|
||||
to_struct(attributes)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
@ -107,4 +108,9 @@ defmodule Livebook.Secrets do
|
|||
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}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,23 +3,27 @@ defmodule Livebook.Secrets.Secret do
|
|||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Livebook.EctoTypes.SecretOrigin
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
name: String.t(),
|
||||
value: String.t()
|
||||
name: String.t() | nil,
|
||||
value: String.t() | nil,
|
||||
origin: SecretOrigin.t()
|
||||
}
|
||||
|
||||
@primary_key {:name, :string, autogenerate: false}
|
||||
embedded_schema do
|
||||
field :value, :string
|
||||
field :origin, SecretOrigin, default: :app
|
||||
end
|
||||
|
||||
def changeset(secret, attrs \\ %{}) do
|
||||
secret
|
||||
|> cast(attrs, [:name, :value])
|
||||
|> cast(attrs, [:name, :value, :origin])
|
||||
|> update_change(:name, &String.upcase/1)
|
||||
|> validate_format(:name, ~r/^\w+$/,
|
||||
message: "should contain only alphanumeric characters and underscore"
|
||||
)
|
||||
|> validate_required([:name, :value])
|
||||
|> validate_required([:name, :value, :origin])
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -62,8 +62,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),
|
||||
livebook_secrets: Secrets.fetch_secrets() |> Map.new(&{&1.name, &1.value}),
|
||||
hub_secrets: get_hub_secrets(),
|
||||
saved_secrets: get_saved_secrets(),
|
||||
select_secret_ref: nil,
|
||||
select_secret_options: nil
|
||||
)
|
||||
|
|
@ -184,8 +183,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
<div data-el-secrets-list>
|
||||
<.secrets_list
|
||||
data_view={@data_view}
|
||||
livebook_secrets={@livebook_secrets}
|
||||
hub_secrets={@hub_secrets}
|
||||
saved_secrets={@saved_secrets}
|
||||
session={@session}
|
||||
socket={@socket}
|
||||
/>
|
||||
|
|
@ -421,7 +419,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
id="secrets"
|
||||
session={@session}
|
||||
secrets={@data_view.secrets}
|
||||
livebook_secrets={@livebook_secrets}
|
||||
saved_secrets={@saved_secrets}
|
||||
prefill_secret_name={@prefill_secret_name}
|
||||
select_secret_ref={@select_secret_ref}
|
||||
select_secret_options={@select_secret_options}
|
||||
|
|
@ -575,7 +573,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
<span class="text-sm text-gray-500">Available only to this session</span>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col space-y-4 mt-6">
|
||||
<%= for {secret_name, secret_value} <- session_only_secrets(@data_view.secrets, @livebook_secrets) do %>
|
||||
<%= for {secret_name, secret_value} <- Enum.sort(@data_view.secrets) do %>
|
||||
<div
|
||||
class="flex flex-col text-gray-500 rounded-lg px-2 pt-1"
|
||||
id={"session-secret-#{secret_name}-wrapper"}
|
||||
|
|
@ -648,7 +646,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
App secrets
|
||||
</h3>
|
||||
<span class="text-sm text-gray-500">
|
||||
<%= if @livebook_secrets == [] do %>
|
||||
<%= if @saved_secrets == [] do %>
|
||||
No secrets stored in Livebook so far
|
||||
<% else %>
|
||||
Toggle to share with this session
|
||||
|
|
@ -657,81 +655,8 @@ defmodule LivebookWeb.SessionLive do
|
|||
</div>
|
||||
|
||||
<div class="flex flex-col space-y-4 mt-6">
|
||||
<%= for {secret_name, secret_value} = secret <- Enum.sort(@livebook_secrets) do %>
|
||||
<div
|
||||
class="flex flex-col text-gray-500 rounded-lg px-2 pt-1"
|
||||
id={"app-secret-#{secret_name}-wrapper"}
|
||||
>
|
||||
<div class="flex" id={"app-secret-#{secret_name}-title"}>
|
||||
<span
|
||||
class="text-sm font-mono break-all w-full cursor-pointer flex flex-row justify-between items-center hover:text-gray-800"
|
||||
phx-click={
|
||||
JS.toggle(to: "#app-secret-#{secret_name}-title", display: "flex")
|
||||
|> JS.toggle(to: "#app-secret-#{secret_name}-detail", display: "flex")
|
||||
|> JS.add_class("bg-gray-100",
|
||||
to: "#app-secret-#{secret_name}-wrapper"
|
||||
)
|
||||
}
|
||||
>
|
||||
<%= secret_name %>
|
||||
</span>
|
||||
<.switch_checkbox
|
||||
name="toggle_secret"
|
||||
checked={is_secret_on_session?(secret, @data_view.secrets)}
|
||||
phx-click="toggle_secret"
|
||||
phx-value-secret_name={secret_name}
|
||||
phx-value-secret_value={secret_value}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col text-gray-800 hidden" id={"app-secret-#{secret_name}-detail"}>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex justify-between items-center">
|
||||
<span
|
||||
class="text-sm font-mono w-full break-all flex-row cursor-pointer"
|
||||
phx-click={
|
||||
JS.toggle(to: "#app-secret-#{secret_name}-title", display: "flex")
|
||||
|> JS.toggle(to: "#app-secret-#{secret_name}-detail", display: "flex")
|
||||
|> JS.remove_class("bg-gray-100",
|
||||
to: "#app-secret-#{secret_name}-wrapper"
|
||||
)
|
||||
}
|
||||
>
|
||||
<%= secret_name %>
|
||||
</span>
|
||||
<.switch_checkbox
|
||||
name="toggle_secret"
|
||||
checked={is_secret_on_session?(secret, @data_view.secrets)}
|
||||
phx-click="toggle_secret"
|
||||
phx-value-secret_name={secret_name}
|
||||
phx-value-secret_value={secret_value}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-row justify-between items-center my-1">
|
||||
<span class="text-sm font-mono break-all flex-row">
|
||||
<%= secret_value %>
|
||||
</span>
|
||||
<%= if Secrets.secret_exists?(secret_name) do %>
|
||||
<button
|
||||
id={"app-secret-#{secret_name}-delete"}
|
||||
type="button"
|
||||
phx-click={
|
||||
with_confirm(
|
||||
JS.push("delete_app_secret", value: %{secret_name: secret_name}),
|
||||
title: "Delete app secret - #{secret_name}",
|
||||
description: "Are you sure you want to delete this app secret?",
|
||||
confirm_text: "Delete",
|
||||
confirm_icon: "delete-bin-6-line"
|
||||
)
|
||||
}
|
||||
class="hover:text-red-600"
|
||||
>
|
||||
<.remix_icon icon="delete-bin-line" />
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%= for secret when secret.origin in [:app, :startup] <- @saved_secrets do %>
|
||||
<.secrets_item secret={secret} prefix="app" data_secrets={@data_view.secrets} />
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
|
|
@ -740,51 +665,18 @@ defmodule LivebookWeb.SessionLive do
|
|||
<h3 class="uppercase text-sm font-semibold text-gray-500">
|
||||
Hub secrets
|
||||
</h3>
|
||||
<span class="text-sm text-gray-500">Available in all sessions</span>
|
||||
<span class="text-sm text-gray-500">
|
||||
<%= if @saved_secrets == [] do %>
|
||||
No secrets stored in Livebook so far
|
||||
<% else %>
|
||||
Toggle to share with this session
|
||||
<% end %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col space-y-4 mt-6">
|
||||
<%= for {secret_name, secret_value} <- Enum.sort(@hub_secrets) do %>
|
||||
<div
|
||||
class="flex flex-col text-gray-500 rounded-lg px-2 pt-1"
|
||||
id={"enterprise-secret-#{secret_name}-wrapper"}
|
||||
>
|
||||
<span
|
||||
class="text-sm font-mono break-all w-full cursor-pointer hover:text-gray-800"
|
||||
id={"enterprise-secret-#{secret_name}-title"}
|
||||
phx-click={
|
||||
JS.toggle(to: "#enterprise-secret-#{secret_name}-title")
|
||||
|> JS.toggle(to: "#enterprise-secret-#{secret_name}-detail")
|
||||
|> JS.add_class("bg-gray-100",
|
||||
to: "#enterprise-secret-#{secret_name}-wrapper"
|
||||
)
|
||||
}
|
||||
>
|
||||
<%= secret_name %>
|
||||
</span>
|
||||
<div
|
||||
class="flex flex-col text-gray-800 hidden"
|
||||
id={"enterprise-secret-#{secret_name}-detail"}
|
||||
phx-click={
|
||||
JS.toggle(to: "#enterprise-secret-#{secret_name}-title")
|
||||
|> JS.toggle(to: "#enterprise-secret-#{secret_name}-detail")
|
||||
|> JS.remove_class("bg-gray-100",
|
||||
to: "#enterprise-secret-#{secret_name}-wrapper"
|
||||
)
|
||||
}
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-sm font-mono break-all flex-row cursor-pointer">
|
||||
<%= secret_name %>
|
||||
</span>
|
||||
<div class="flex flex-row justify-between items-center my-1">
|
||||
<span class="text-sm font-mono break-all flex-row">
|
||||
<%= secret_value %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%= for %{origin: {:hub, id}} = secret <- @saved_secrets do %>
|
||||
<.secrets_item secret={secret} prefix={"hub-#{id}"} data_secrets={@data_view.secrets} />
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
@ -793,6 +685,89 @@ defmodule LivebookWeb.SessionLive do
|
|||
"""
|
||||
end
|
||||
|
||||
defp secrets_item(assigns) do
|
||||
~H"""
|
||||
<div
|
||||
class="flex flex-col text-gray-500 rounded-lg px-2 pt-1"
|
||||
id={"#{@prefix}-secret-#{@secret.name}-wrapper"}
|
||||
>
|
||||
<div class="flex" id={"#{@prefix}-secret-#{@secret.name}-title"}>
|
||||
<span
|
||||
class="text-sm font-mono break-all w-full cursor-pointer flex flex-row justify-between items-center hover:text-gray-800"
|
||||
phx-click={
|
||||
JS.toggle(to: "##{@prefix}-secret-#{@secret.name}-title", display: "flex")
|
||||
|> JS.toggle(to: "##{@prefix}-secret-#{@secret.name}-detail", display: "flex")
|
||||
|> JS.add_class("bg-gray-100",
|
||||
to: "##{@prefix}-secret-#{@secret.name}-wrapper"
|
||||
)
|
||||
}
|
||||
>
|
||||
<%= @secret.name %>
|
||||
</span>
|
||||
<%= if @secret.origin in [:app, :startup] do %>
|
||||
<.switch_checkbox
|
||||
name="toggle_secret"
|
||||
checked={is_secret_on_session?(@secret, @data_secrets)}
|
||||
phx-click="toggle_secret"
|
||||
phx-value-secret_name={@secret.name}
|
||||
phx-value-secret_value={@secret.value}
|
||||
/>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex flex-col text-gray-800 hidden" id={"#{@prefix}-secret-#{@secret.name}-detail"}>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex justify-between items-center">
|
||||
<span
|
||||
class="text-sm font-mono w-full break-all flex-row cursor-pointer"
|
||||
phx-click={
|
||||
JS.toggle(to: "##{@prefix}-secret-#{@secret.name}-title", display: "flex")
|
||||
|> JS.toggle(to: "##{@prefix}-secret-#{@secret.name}-detail", display: "flex")
|
||||
|> JS.remove_class("bg-gray-100",
|
||||
to: "##{@prefix}-secret-#{@secret.name}-wrapper"
|
||||
)
|
||||
}
|
||||
>
|
||||
<%= @secret.name %>
|
||||
</span>
|
||||
<%= if @secret.origin in [:app, :startup] do %>
|
||||
<.switch_checkbox
|
||||
name="toggle_secret"
|
||||
checked={is_secret_on_session?(@secret, @data_secrets)}
|
||||
phx-click="toggle_secret"
|
||||
phx-value-secret_name={@secret.name}
|
||||
phx-value-secret_value={@secret.value}
|
||||
/>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex flex-row justify-between items-center my-1">
|
||||
<span class="text-sm font-mono break-all flex-row">
|
||||
<%= @secret.value %>
|
||||
</span>
|
||||
<%= if @secret.origin == :app do %>
|
||||
<button
|
||||
id={"#{@prefix}-secret-#{@secret.name}-delete"}
|
||||
type="button"
|
||||
phx-click={
|
||||
with_confirm(
|
||||
JS.push("delete_app_secret", value: %{secret_name: @secret.name}),
|
||||
title: "Delete app secret - #{@secret.name}",
|
||||
description: "Are you sure you want to delete this app secret?",
|
||||
confirm_text: "Delete",
|
||||
confirm_icon: "delete-bin-6-line"
|
||||
)
|
||||
}
|
||||
class="hover:text-red-600"
|
||||
>
|
||||
<.remix_icon icon="delete-bin-line" />
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp secrets_info_icon(assigns) do
|
||||
~H"""
|
||||
<span
|
||||
|
|
@ -1446,17 +1421,17 @@ defmodule LivebookWeb.SessionLive do
|
|||
{:noreply, handle_operation(socket, operation)}
|
||||
end
|
||||
|
||||
def handle_info({:secret_created, %Secrets.Secret{}}, socket) do
|
||||
def handle_info({:secret_created, %{origin: {:hub, _id}}}, socket) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(hub_secrets: get_hub_secrets())
|
||||
|> assign(saved_secrets: get_saved_secrets())
|
||||
|> put_flash(:info, "A new secret has been created on your Livebook Enterprise")}
|
||||
end
|
||||
|
||||
def handle_info({:secret_updated, %Secrets.Secret{}}, socket) do
|
||||
def handle_info({:secret_updated, %{origin: {:hub, _id}}}, socket) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(hub_secrets: get_hub_secrets())
|
||||
|> assign(saved_secrets: get_saved_secrets())
|
||||
|> put_flash(:info, "An existing secret has been updated on your Livebook Enterprise")}
|
||||
end
|
||||
|
||||
|
|
@ -1543,15 +1518,23 @@ defmodule LivebookWeb.SessionLive do
|
|||
end
|
||||
|
||||
def handle_info({:set_secret, secret}, socket) do
|
||||
livebook_secrets = Map.put(socket.assigns.livebook_secrets, secret.name, secret.value)
|
||||
saved_secrets =
|
||||
Enum.reject(
|
||||
socket.assigns.saved_secrets,
|
||||
&(&1.name == secret.name and &1.origin == secret.origin)
|
||||
)
|
||||
|
||||
{:noreply, assign(socket, livebook_secrets: livebook_secrets)}
|
||||
{:noreply, assign(socket, saved_secrets: [secret | saved_secrets])}
|
||||
end
|
||||
|
||||
def handle_info({:unset_secret, secret}, socket) do
|
||||
livebook_secrets = Map.delete(socket.assigns.livebook_secrets, secret.name)
|
||||
saved_secrets =
|
||||
Enum.reject(
|
||||
socket.assigns.saved_secrets,
|
||||
&(&1.name == secret.name and &1.origin == secret.origin)
|
||||
)
|
||||
|
||||
{:noreply, assign(socket, livebook_secrets: livebook_secrets)}
|
||||
{:noreply, assign(socket, saved_secrets: saved_secrets)}
|
||||
end
|
||||
|
||||
def handle_info(_message, socket), do: {:noreply, socket}
|
||||
|
|
@ -2289,18 +2272,16 @@ defmodule LivebookWeb.SessionLive do
|
|||
:ok
|
||||
end
|
||||
|
||||
defp session_only_secrets(secrets, livebook_secrets) do
|
||||
Enum.reject(secrets, &(&1 in livebook_secrets)) |> Enum.sort()
|
||||
end
|
||||
|
||||
defp is_secret_on_session?(secret, secrets) do
|
||||
secret in secrets
|
||||
Map.has_key?(secrets, secret.name)
|
||||
end
|
||||
|
||||
defp get_hub_secrets do
|
||||
for connected_hub <- Hubs.get_connected_hubs(),
|
||||
secret <- EnterpriseClient.list_cached_secrets(connected_hub.pid),
|
||||
into: %{},
|
||||
do: {secret.name, secret.value}
|
||||
defp get_saved_secrets do
|
||||
hub_secrets =
|
||||
for connected_hub <- Hubs.get_connected_hubs(),
|
||||
secret <- EnterpriseClient.list_cached_secrets(connected_hub.pid),
|
||||
do: secret
|
||||
|
||||
Enum.sort(hub_secrets ++ Secrets.get_secrets())
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
|
|||
use LivebookWeb, :live_component
|
||||
|
||||
alias Livebook.Hubs.EnterpriseClient
|
||||
alias Livebook.Secrets.Secret
|
||||
|
||||
@impl true
|
||||
def update(assigns, socket) do
|
||||
|
|
@ -49,22 +50,24 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
|
|||
<%= for {secret_name, _} <- Enum.sort(@secrets) do %>
|
||||
<.secret_with_badge
|
||||
secret_name={secret_name}
|
||||
origin="session"
|
||||
stored="Session"
|
||||
action="select_secret"
|
||||
active={secret_name == @prefill_secret_name}
|
||||
target={@myself}
|
||||
/>
|
||||
<% end %>
|
||||
<%= for {secret_name, _} <- livebook_only_secrets(@secrets, @livebook_secrets) do %>
|
||||
<%= for secret <- @saved_secrets do %>
|
||||
<.secret_with_badge
|
||||
secret_name={secret_name}
|
||||
stored="Livebook"
|
||||
action="select_livebook_secret"
|
||||
secret_name={secret.name}
|
||||
origin={origin(secret)}
|
||||
stored={stored(secret)}
|
||||
action="select_secret"
|
||||
active={false}
|
||||
target={@myself}
|
||||
/>
|
||||
<% end %>
|
||||
<%= if @secrets == %{} and @livebook_secrets == %{} do %>
|
||||
<%= if @secrets == %{} and @saved_secrets == [] do %>
|
||||
<div class="w-full text-center text-gray-400 border rounded-lg p-8">
|
||||
<.remix_icon icon="folder-lock-line" class="align-middle text-2xl" />
|
||||
<span class="mt-1 block text-sm text-gray-700">
|
||||
|
|
@ -119,7 +122,7 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
|
|||
<%= radio_button(f, :store, "session", checked: @data["store"] == "session") %> only this session
|
||||
<% end %>
|
||||
<%= label class: "flex items-center gap-2 text-gray-600" do %>
|
||||
<%= radio_button(f, :store, "livebook", checked: @data["store"] == "livebook") %> in the Livebook app
|
||||
<%= radio_button(f, :store, "app", checked: @data["store"] == "app") %> in the Livebook app
|
||||
<% end %>
|
||||
<%= if Livebook.Config.feature_flag_enabled?(:hub) do %>
|
||||
<%= label class: "flex items-center gap-2 text-gray-600" do %>
|
||||
|
|
@ -166,6 +169,7 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
|
|||
end
|
||||
]}
|
||||
phx-value-secret_name={@secret_name}
|
||||
phx-value-origin={@origin}
|
||||
phx-target={@target}
|
||||
phx-click={@action}
|
||||
>
|
||||
|
|
@ -221,10 +225,17 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
|
|||
"""
|
||||
end
|
||||
|
||||
defp origin(%{origin: {:hub, id}}), do: id
|
||||
defp origin(%{origin: origin}), do: to_string(origin)
|
||||
|
||||
defp stored(%{origin: {:hub, _}}), do: "Hub"
|
||||
defp stored(%{origin: origin}) when origin in [:app, :startup], do: "Livebook"
|
||||
|
||||
@impl true
|
||||
def handle_event("save", %{"data" => data}, socket) do
|
||||
with {:ok, secret} <- Livebook.Secrets.validate_secret(data),
|
||||
:ok <- set_secret(socket, secret, data["store"]) do
|
||||
with attrs <- build_attrs(data),
|
||||
{:ok, secret} <- Livebook.Secrets.validate_secret(attrs),
|
||||
:ok <- set_secret(socket, secret) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> push_patch(to: socket.assigns.return_to)
|
||||
|
|
@ -238,32 +249,51 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
|
|||
end
|
||||
end
|
||||
|
||||
def handle_event("select_secret", %{"secret_name" => secret_name}, socket) do
|
||||
def handle_event(
|
||||
"select_secret",
|
||||
%{"secret_name" => secret_name, "origin" => "session"},
|
||||
socket
|
||||
) do
|
||||
{:noreply,
|
||||
socket |> push_patch(to: socket.assigns.return_to) |> push_secret_selected(secret_name)}
|
||||
socket
|
||||
|> push_patch(to: socket.assigns.return_to)
|
||||
|> push_secret_selected(secret_name)}
|
||||
end
|
||||
|
||||
def handle_event("select_livebook_secret", %{"secret_name" => secret_name}, socket) do
|
||||
grant_access(secret_name, socket)
|
||||
def handle_event("select_secret", %{"secret_name" => secret_name, "origin" => "app"}, socket) do
|
||||
grant_access(socket.assigns.saved_secrets, secret_name, :app, socket)
|
||||
|
||||
{:noreply,
|
||||
socket |> push_patch(to: socket.assigns.return_to) |> push_secret_selected(secret_name)}
|
||||
socket
|
||||
|> push_patch(to: socket.assigns.return_to)
|
||||
|> push_secret_selected(secret_name)}
|
||||
end
|
||||
|
||||
def handle_event("select_secret", %{"secret_name" => secret_name, "origin" => hub_id}, socket) do
|
||||
grant_access(socket.assigns.saved_secrets, secret_name, {:hub, hub_id}, socket)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> push_patch(to: socket.assigns.return_to)
|
||||
|> push_secret_selected(secret_name)}
|
||||
end
|
||||
|
||||
def handle_event("validate", %{"data" => data}, socket) do
|
||||
socket = assign(socket, data: data)
|
||||
|
||||
case Livebook.Secrets.validate_secret(data) do
|
||||
{:ok, _} -> {:noreply, assign(socket, errors: [])}
|
||||
{:ok, _secret} -> {:noreply, assign(socket, errors: [])}
|
||||
{:error, changeset} -> {:noreply, assign(socket, errors: changeset.errors)}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("grant_access", %{"secret_name" => secret_name}, socket) do
|
||||
grant_access(secret_name, socket)
|
||||
grant_access(socket.assigns.saved_secrets, secret_name, :app, socket)
|
||||
|
||||
{:noreply,
|
||||
socket |> push_patch(to: socket.assigns.return_to) |> push_secret_selected(secret_name)}
|
||||
socket
|
||||
|> push_patch(to: socket.assigns.return_to)
|
||||
|> push_secret_selected(secret_name)}
|
||||
end
|
||||
|
||||
defp push_secret_selected(%{assigns: %{select_secret_ref: nil}} = socket, _), do: socket
|
||||
|
|
@ -273,40 +303,43 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
|
|||
end
|
||||
|
||||
defp prefill_secret_name(socket) do
|
||||
if unavailable_secret?(
|
||||
socket.assigns.prefill_secret_name,
|
||||
socket.assigns.secrets,
|
||||
socket.assigns.livebook_secrets
|
||||
),
|
||||
do: socket.assigns.prefill_secret_name,
|
||||
else: ""
|
||||
if unavailable_secret?(socket, socket.assigns.prefill_secret_name),
|
||||
do: socket.assigns.prefill_secret_name,
|
||||
else: ""
|
||||
end
|
||||
|
||||
defp unavailable_secret?(nil, _, _), do: false
|
||||
defp unavailable_secret?("", _, _), do: false
|
||||
defp unavailable_secret?(_socket, nil), do: false
|
||||
defp unavailable_secret?(_socket, ""), do: false
|
||||
|
||||
defp unavailable_secret?(preselect_name, secrets, livebook_secrets) do
|
||||
not Map.has_key?(secrets, preselect_name) and
|
||||
not Map.has_key?(livebook_secrets, preselect_name)
|
||||
defp unavailable_secret?(socket, preselect_name) do
|
||||
not session?(socket, preselect_name) and
|
||||
not app?(socket, preselect_name) and
|
||||
not hub?(socket, preselect_name)
|
||||
end
|
||||
|
||||
defp title(%{assigns: %{select_secret_ref: nil}}), do: "Add secret"
|
||||
defp title(%{assigns: %{select_secret_options: %{"title" => title}}}), do: title
|
||||
defp title(_), do: "Select secret"
|
||||
|
||||
defp set_secret(socket, secret, "session") do
|
||||
defp build_origin(%{"store" => "session"}), do: :session
|
||||
defp build_origin(%{"store" => "app"}), do: :app
|
||||
defp build_origin(%{"store" => "hub", "connected_hub" => id}), do: {:hub, id}
|
||||
|
||||
defp build_attrs(%{"name" => name, "value" => value} = attrs) do
|
||||
%{name: name, value: value, origin: build_origin(attrs)}
|
||||
end
|
||||
|
||||
defp set_secret(socket, %Secret{origin: :session} = secret) do
|
||||
Livebook.Session.set_secret(socket.assigns.session.pid, secret)
|
||||
end
|
||||
|
||||
defp set_secret(socket, secret, "livebook") do
|
||||
defp set_secret(socket, %Secret{origin: :app} = secret) do
|
||||
Livebook.Secrets.set_secret(secret)
|
||||
Livebook.Session.set_secret(socket.assigns.session.pid, secret)
|
||||
end
|
||||
|
||||
defp set_secret(socket, secret, "hub") do
|
||||
selected_hub = socket.assigns.data["connected_hub"]
|
||||
|
||||
if hub = Enum.find(socket.assigns.connected_hubs, &(&1.hub.id == selected_hub)) do
|
||||
defp set_secret(socket, %Secret{origin: {:hub, id}} = secret) when is_binary(id) do
|
||||
if hub = Enum.find(socket.assigns.connected_hubs, &(&1.hub.id == id)) do
|
||||
create_secret_request =
|
||||
LivebookProto.CreateSecretRequest.new!(
|
||||
name: secret.name,
|
||||
|
|
@ -322,23 +355,36 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
|
|||
end
|
||||
end
|
||||
|
||||
defp grant_access(secret_name, socket) do
|
||||
secret_value = socket.assigns.livebook_secrets[secret_name]
|
||||
secret = %{name: secret_name, value: secret_value}
|
||||
set_secret(socket.assigns.session.pid, secret, "session")
|
||||
defp grant_access(secrets, secret_name, origin, socket) do
|
||||
secret = Enum.find(secrets, &(&1.name == secret_name and &1.origin == origin))
|
||||
|
||||
if secret,
|
||||
do: set_secret(socket, secret),
|
||||
else: :ok
|
||||
end
|
||||
|
||||
defp livebook_only_secrets(secrets, livebook_secrets) do
|
||||
Enum.reject(livebook_secrets, &(&1 in secrets)) |> Enum.sort()
|
||||
end
|
||||
|
||||
defp must_grant_access(%{assigns: %{prefill_secret_name: prefill_secret_name}} = socket) do
|
||||
if not Map.has_key?(socket.assigns.secrets, prefill_secret_name) and
|
||||
Map.has_key?(socket.assigns.livebook_secrets, prefill_secret_name) do
|
||||
prefill_secret_name
|
||||
defp must_grant_access(%{assigns: %{prefill_secret_name: secret_name}} = socket) do
|
||||
if not session?(socket, secret_name) and
|
||||
(app?(socket, secret_name) or hub?(socket, secret_name)) do
|
||||
secret_name
|
||||
end
|
||||
end
|
||||
|
||||
defp session?(socket, secret_name) do
|
||||
Enum.any?(socket.assigns.secrets, &(elem(&1, 0) == secret_name))
|
||||
end
|
||||
|
||||
defp app?(socket, secret_name) do
|
||||
Enum.any?(
|
||||
socket.assigns.saved_secrets,
|
||||
&(&1.name == secret_name and &1.origin in [:app, :startup])
|
||||
)
|
||||
end
|
||||
|
||||
defp hub?(socket, secret_name) do
|
||||
Enum.any?(socket.assigns.saved_secrets, &(&1.name == secret_name))
|
||||
end
|
||||
|
||||
# TODO: Livebook.Hubs.fetch_hubs_with_secrets_storage()
|
||||
defp connected_hubs_options(connected_hubs, selected_hub) do
|
||||
[[key: "Select one Hub", value: "", selected: true, disabled: true]] ++
|
||||
|
|
|
|||
|
|
@ -35,35 +35,42 @@ defmodule Livebook.Hubs.EnterpriseClientTest do
|
|||
end
|
||||
|
||||
describe "handle events" do
|
||||
setup %{url: url, token: token} do
|
||||
enterprise = build(:enterprise, url: url, token: token)
|
||||
EnterpriseClient.start_link(enterprise)
|
||||
setup %{test: test, url: url, token: token} do
|
||||
node = EnterpriseServer.get_node()
|
||||
hub_id = "enterprise-#{test}"
|
||||
|
||||
insert_hub(:enterprise,
|
||||
id: hub_id,
|
||||
external_id: to_string(test),
|
||||
url: url,
|
||||
token: token
|
||||
)
|
||||
|
||||
assert_receive :hub_connected
|
||||
|
||||
:ok
|
||||
{:ok, node: node, hub_id: hub_id}
|
||||
end
|
||||
|
||||
test "receives a secret_created event" do
|
||||
test "receives a secret_created event", %{node: node, hub_id: id} do
|
||||
name = "API_TOKEN_ID"
|
||||
value = Livebook.Utils.random_id()
|
||||
node = EnterpriseServer.get_node()
|
||||
:erpc.call(node, Enterprise.Integration, :create_secret, [name, value])
|
||||
|
||||
assert_receive {:secret_created, %Secret{name: ^name, value: ^value}}
|
||||
assert_receive {:secret_created, %Secret{name: ^name, value: ^value, origin: {:hub, ^id}}}
|
||||
end
|
||||
|
||||
test "receives a secret_updated event" do
|
||||
test "receives a secret_updated event", %{node: node, hub_id: id} do
|
||||
name = "SUPER_SUDO_USER"
|
||||
value = "JakePeralta"
|
||||
node = EnterpriseServer.get_node()
|
||||
secret = :erpc.call(node, Enterprise.Integration, :create_secret, [name, value])
|
||||
|
||||
assert_receive {:secret_created, %Secret{name: ^name, value: ^value}}
|
||||
assert_receive {:secret_created, %Secret{name: ^name, value: ^value, origin: {:hub, ^id}}}
|
||||
|
||||
new_value = "ChonkyCat"
|
||||
:erpc.call(node, Enterprise.Integration, :update_secret, [secret, new_value])
|
||||
|
||||
assert_receive {:secret_updated, %Secret{name: ^name, value: ^new_value}}
|
||||
assert_receive {:secret_updated,
|
||||
%Secret{name: ^name, value: ^new_value, origin: {:hub, ^id}}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,35 +3,33 @@ defmodule Livebook.SecretsTest do
|
|||
use Livebook.DataCase
|
||||
|
||||
alias Livebook.Secrets
|
||||
alias Livebook.Secrets.Secret
|
||||
|
||||
describe "fetch_secrets/0" do
|
||||
describe "get_secrets/0" do
|
||||
test "returns a list of secrets from storage" do
|
||||
secret = %Secret{name: "FOO", value: "111"}
|
||||
secret = build(:secret, name: "FOO", value: "111")
|
||||
|
||||
Secrets.set_secret(secret)
|
||||
assert secret in Secrets.fetch_secrets()
|
||||
assert secret in Secrets.get_secrets()
|
||||
|
||||
Secrets.unset_secret(secret.name)
|
||||
refute secret in Secrets.fetch_secrets()
|
||||
refute secret in Secrets.get_secrets()
|
||||
end
|
||||
|
||||
test "returns a list of secrets from temporary storage" do
|
||||
secret = %Secret{name: "BAR", value: "222"}
|
||||
secret = build(:secret, name: "FOO", value: "222", origin: :startup)
|
||||
|
||||
Secrets.set_temporary_secrets([secret])
|
||||
assert secret in Secrets.fetch_secrets()
|
||||
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.fetch_secrets()
|
||||
assert secret in Secrets.get_secrets()
|
||||
end
|
||||
end
|
||||
|
||||
test "fetch an specific secret" do
|
||||
secret = %Secret{name: "FOO", value: "111"}
|
||||
Secrets.set_secret(secret)
|
||||
secret = insert_secret(name: "FOO", value: "111")
|
||||
|
||||
assert_raise Livebook.Storage.NotFoundError,
|
||||
~s(could not find entry in \"secrets\" with ID "NOT_HERE"),
|
||||
|
|
@ -39,32 +37,36 @@ defmodule Livebook.SecretsTest do
|
|||
Secrets.fetch_secret!("NOT_HERE")
|
||||
end
|
||||
|
||||
assert Secrets.fetch_secret!(secret.name) == %Secret{name: "FOO", value: "111"}
|
||||
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")
|
||||
Secrets.set_secret(%Secret{name: "FOO", value: "111"})
|
||||
|
||||
insert_secret(name: "FOO", value: "111")
|
||||
|
||||
assert Secrets.secret_exists?("FOO")
|
||||
Secrets.unset_secret("FOO")
|
||||
end
|
||||
|
||||
describe "validate_secret/1" do
|
||||
test "returns a valid secret" do
|
||||
attrs = %{name: "FOO", value: "111"}
|
||||
attrs = params_for(:secret, name: "FOO", value: "111")
|
||||
|
||||
assert {:ok, secret} = Secrets.validate_secret(attrs)
|
||||
assert attrs.name == secret.name
|
||||
assert attrs.value == secret.value
|
||||
assert attrs.origin == secret.origin
|
||||
end
|
||||
|
||||
test "returns changeset error" do
|
||||
attrs = %{value: "111"}
|
||||
attrs = params_for(:secret, name: nil, value: "111")
|
||||
assert {:error, changeset} = Secrets.validate_secret(attrs)
|
||||
assert "can't be blank" in errors_on(changeset).name
|
||||
attrs = %{name: "@inavalid", value: "111"}
|
||||
|
||||
attrs = params_for(:secret, name: "@inavalid", value: "111")
|
||||
assert {:error, changeset} = Secrets.validate_secret(attrs)
|
||||
|
||||
assert "should contain only alphanumeric characters and underscore" in errors_on(changeset).name
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ defmodule Livebook.WebSocket.ClientConnectionTest do
|
|||
headers = [{"X-Auth-Token", token}]
|
||||
|
||||
{:ok, conn} = ClientConnection.start_link(self(), url, headers)
|
||||
|
||||
assert_receive {:connect, :ok, :connected}
|
||||
|
||||
{:ok, conn: conn}
|
||||
|
|
@ -80,27 +79,16 @@ defmodule Livebook.WebSocket.ClientConnectionTest do
|
|||
|
||||
describe "reconnect event" do
|
||||
setup %{test: name} do
|
||||
suffix = Ecto.UUID.generate() |> :erlang.phash2() |> to_string()
|
||||
app_port = Enum.random(1000..9000) |> to_string()
|
||||
|
||||
{:ok, _} =
|
||||
EnterpriseServer.start(name,
|
||||
env: %{"ENTERPRISE_DB_SUFFIX" => suffix},
|
||||
app_port: app_port
|
||||
)
|
||||
start_new_instance(name)
|
||||
|
||||
url = EnterpriseServer.url(name)
|
||||
token = EnterpriseServer.token(name)
|
||||
headers = [{"X-Auth-Token", token}]
|
||||
|
||||
assert {:ok, conn} = ClientConnection.start_link(self(), url, headers)
|
||||
|
||||
assert_receive {:connect, :ok, :connected}
|
||||
|
||||
on_exit(fn ->
|
||||
EnterpriseServer.disconnect(name)
|
||||
EnterpriseServer.drop_database(name)
|
||||
end)
|
||||
on_exit(fn -> stop_new_instance(name) end)
|
||||
|
||||
{:ok, conn: conn}
|
||||
end
|
||||
|
|
@ -134,7 +122,6 @@ defmodule Livebook.WebSocket.ClientConnectionTest do
|
|||
headers = [{"X-Auth-Token", token}]
|
||||
|
||||
{:ok, _conn} = ClientConnection.start_link(self(), url, headers)
|
||||
|
||||
assert_receive {:connect, :ok, :connected}
|
||||
|
||||
:ok
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ defmodule LivebookWeb.Hub.New.EnterpriseComponentTest do
|
|||
describe "enterprise" do
|
||||
test "persists new hub", %{conn: conn, url: url, token: token} do
|
||||
node = EnterpriseServer.get_node()
|
||||
id = :erpc.call(node, Enterprise.Integration, :fetch_env!, [])
|
||||
id = :erpc.call(node, Enterprise.Integration, :fetch_env!, ["ENTERPRISE_ID"])
|
||||
Livebook.Hubs.delete_hub("enterprise-#{id}")
|
||||
|
||||
{:ok, view, _html} = live(conn, Routes.hub_path(conn, :new))
|
||||
|
|
@ -101,7 +101,7 @@ defmodule LivebookWeb.Hub.New.EnterpriseComponentTest do
|
|||
url = EnterpriseServer.url(name)
|
||||
token = EnterpriseServer.token(name)
|
||||
|
||||
id = :erpc.call(node, Enterprise.Integration, :fetch_env!, [])
|
||||
id = :erpc.call(node, Enterprise.Integration, :fetch_env!, ["ENTERPRISE_ID"])
|
||||
user = :erpc.call(node, Enterprise.Integration, :create_user, [])
|
||||
|
||||
another_token =
|
||||
|
|
@ -165,20 +165,4 @@ defmodule LivebookWeb.Hub.New.EnterpriseComponentTest do
|
|||
stop_new_instance(name)
|
||||
end
|
||||
end
|
||||
|
||||
defp start_new_instance(name) do
|
||||
suffix = Ecto.UUID.generate() |> :erlang.phash2() |> to_string()
|
||||
app_port = Enum.random(1000..9000) |> to_string()
|
||||
|
||||
{:ok, _} =
|
||||
EnterpriseServer.start(name,
|
||||
env: %{"ENTERPRISE_DB_SUFFIX" => suffix},
|
||||
app_port: app_port
|
||||
)
|
||||
end
|
||||
|
||||
defp stop_new_instance(name) do
|
||||
EnterpriseServer.disconnect(name)
|
||||
EnterpriseServer.drop_database(name)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,26 +3,27 @@ defmodule LivebookWeb.SessionLive.SecretsComponentTest do
|
|||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Livebook.Secrets.Secret
|
||||
alias Livebook.Session
|
||||
alias Livebook.Sessions
|
||||
|
||||
describe "enterprise" do
|
||||
setup %{url: url, token: token} do
|
||||
node = EnterpriseServer.get_node()
|
||||
id = :erpc.call(node, Enterprise.Integration, :fetch_env!, [])
|
||||
Livebook.Hubs.delete_hub("enterprise-#{id}")
|
||||
id = :erpc.call(node, Enterprise.Integration, :fetch_env!, ["ENTERPRISE_ID"])
|
||||
hub_id = "enterprise-#{id}"
|
||||
|
||||
Livebook.Hubs.subscribe([:connection, :secrets])
|
||||
Livebook.Hubs.delete_hub(hub_id)
|
||||
|
||||
enterprise =
|
||||
insert_hub(:enterprise,
|
||||
id: "enterprise-#{id}",
|
||||
id: hub_id,
|
||||
external_id: id,
|
||||
url: url,
|
||||
token: token
|
||||
)
|
||||
|
||||
{:ok, session} = Sessions.create_session(notebook: Livebook.Notebook.new())
|
||||
Livebook.Hubs.subscribe(:secrets)
|
||||
|
||||
on_exit(fn ->
|
||||
Session.close(session.pid)
|
||||
|
|
@ -36,14 +37,15 @@ defmodule LivebookWeb.SessionLive.SecretsComponentTest do
|
|||
session: session,
|
||||
enterprise: enterprise
|
||||
} do
|
||||
secret = build(:secret, name: "LESS_IMPORTANT_SECRET", value: "123", origin: enterprise.id)
|
||||
{:ok, view, _html} = live(conn, Routes.session_path(conn, :secrets, session.id))
|
||||
|
||||
assert view
|
||||
|> element(~s{form[phx-submit="save"]})
|
||||
|> render_change(%{
|
||||
data: %{
|
||||
name: "FOO",
|
||||
value: "123",
|
||||
name: secret.name,
|
||||
value: secret.value,
|
||||
store: "hub"
|
||||
}
|
||||
}) =~ ~s(<option value="#{enterprise.id}">#{enterprise.hub_name}</option>)
|
||||
|
|
@ -54,28 +56,25 @@ defmodule LivebookWeb.SessionLive.SecretsComponentTest do
|
|||
session: session,
|
||||
enterprise: enterprise
|
||||
} do
|
||||
id = enterprise.id
|
||||
secret = build(:secret, name: "BIG_IMPORTANT_SECRET", value: "123", origin: id)
|
||||
{:ok, view, _html} = live(conn, Routes.session_path(conn, :secrets, session.id))
|
||||
|
||||
attrs = %{
|
||||
data: %{
|
||||
name: "FOO",
|
||||
value: "123",
|
||||
name: secret.name,
|
||||
value: secret.value,
|
||||
store: "hub",
|
||||
connected_hub: enterprise.id
|
||||
}
|
||||
}
|
||||
|
||||
view
|
||||
|> element(~s{form[phx-submit="save"]})
|
||||
|> render_change(attrs)
|
||||
form = element(view, ~s{form[phx-submit="save"]})
|
||||
render_change(form, attrs)
|
||||
render_submit(form, attrs)
|
||||
|
||||
view
|
||||
|> element(~s{form[phx-submit="save"]})
|
||||
|> render_submit(attrs)
|
||||
|
||||
assert_receive {:secret_created, %Secret{name: "FOO", value: "123"}}
|
||||
assert render(view) =~ "A new secret has been created on your Livebook Enterprise"
|
||||
assert has_element?(view, "#enterprise-secret-#{attrs.data.name}-title")
|
||||
assert has_element?(view, "#hub-#{enterprise.id}-secret-#{attrs.data.name}-title")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
|
||||
alias Livebook.{Sessions, Session, Settings, Runtime, Users, FileSystem}
|
||||
alias Livebook.Notebook.Cell
|
||||
alias Livebook.Secrets.Secret
|
||||
|
||||
setup do
|
||||
{:ok, session} = Sessions.create_session(notebook: Livebook.Notebook.new())
|
||||
|
|
@ -1051,65 +1050,74 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
describe "secrets" do
|
||||
test "adds a secret from form", %{conn: conn, session: session} do
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session.id}/secrets")
|
||||
secret = build(:secret, name: "FOO", value: "123", origin: :session)
|
||||
|
||||
view
|
||||
|> element(~s{form[phx-submit="save"]})
|
||||
|> render_submit(%{data: %{name: "foo", value: "123", store: "session"}})
|
||||
|> render_submit(%{data: %{name: secret.name, value: secret.value, store: "session"}})
|
||||
|
||||
assert %{secrets: %{"FOO" => "123"}} = Session.get_data(session.pid)
|
||||
assert_session_secret(view, session.pid, secret)
|
||||
end
|
||||
|
||||
test "adds a livebook secret from form", %{conn: conn, session: session} do
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session.id}/secrets")
|
||||
secret = build(:secret, name: "BAR", value: "456", origin: :app)
|
||||
|
||||
view
|
||||
|> element(~s{form[phx-submit="save"]})
|
||||
|> render_submit(%{data: %{name: "bar", value: "456", store: "livebook"}})
|
||||
|> render_submit(%{data: %{name: secret.name, value: secret.value, store: "app"}})
|
||||
|
||||
assert %Secret{name: "BAR", value: "456"} in Livebook.Secrets.fetch_secrets()
|
||||
assert secret in Livebook.Secrets.get_secrets()
|
||||
end
|
||||
|
||||
test "syncs secrets", %{conn: conn, session: session} do
|
||||
Livebook.Secrets.set_secret(%Secret{name: "FOO", value: "123"})
|
||||
session_secret = insert_secret(name: "FOO", value: "123")
|
||||
secret = build(:secret, name: "FOO", value: "456", origin: :app)
|
||||
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session.id}/secrets")
|
||||
|
||||
view
|
||||
|> element(~s{form[phx-submit="save"]})
|
||||
|> render_submit(%{data: %{name: "FOO", value: "456", store: "livebook"}})
|
||||
|> render_submit(%{data: %{name: secret.name, value: secret.value, store: "app"}})
|
||||
|
||||
assert %{secrets: %{"FOO" => "456"}} = Session.get_data(session.pid)
|
||||
assert %Secret{name: "FOO", value: "456"} in Livebook.Secrets.fetch_secrets()
|
||||
assert_session_secret(view, session.pid, secret)
|
||||
assert secret in Livebook.Secrets.get_secrets()
|
||||
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session.id}/secrets")
|
||||
Session.set_secret(session.pid, %{name: "FOO", value: "123"})
|
||||
Session.set_secret(session.pid, session_secret)
|
||||
|
||||
secret = build(:secret, name: "FOO", value: "789", origin: :app)
|
||||
|
||||
view
|
||||
|> element(~s{form[phx-submit="save"]})
|
||||
|> render_submit(%{data: %{name: "FOO", value: "789", store: "livebook"}})
|
||||
|> render_submit(%{data: %{name: secret.name, value: secret.value, store: "app"}})
|
||||
|
||||
assert %{secrets: %{"FOO" => "789"}} = Session.get_data(session.pid)
|
||||
assert %Secret{name: "FOO", value: "789"} in Livebook.Secrets.fetch_secrets()
|
||||
assert_session_secret(view, session.pid, secret)
|
||||
assert secret in Livebook.Secrets.get_secrets()
|
||||
end
|
||||
|
||||
test "never syncs secrets when updating from session", %{conn: conn, session: session} do
|
||||
Livebook.Secrets.set_secret(%Secret{name: "FOO", value: "123"})
|
||||
app_secret = insert_secret(name: "FOO", value: "123")
|
||||
secret = build(:secret, name: "FOO", value: "456", origin: :session)
|
||||
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session.id}/secrets")
|
||||
Session.set_secret(session.pid, %{name: "FOO", value: "123"})
|
||||
Session.set_secret(session.pid, app_secret)
|
||||
|
||||
view
|
||||
|> element(~s{form[phx-submit="save"]})
|
||||
|> render_submit(%{data: %{name: "FOO", value: "456", store: "session"}})
|
||||
|> render_submit(%{data: %{name: secret.name, value: secret.value, store: "session"}})
|
||||
|
||||
assert %{secrets: %{"FOO" => "456"}} = Session.get_data(session.pid)
|
||||
|
||||
refute %Secret{name: "FOO", value: "456"} in Livebook.Secrets.fetch_secrets()
|
||||
assert %Secret{name: "FOO", value: "123"} in Livebook.Secrets.fetch_secrets()
|
||||
assert_session_secret(view, session.pid, secret)
|
||||
refute secret in Livebook.Secrets.get_secrets()
|
||||
assert app_secret in Livebook.Secrets.get_secrets()
|
||||
end
|
||||
|
||||
test "shows the 'Add secret' button for unavailable secrets", %{conn: conn, session: session} do
|
||||
secret = build(:secret, name: "ANOTHER_GREAT_SECRET", value: "123456", origin: :session)
|
||||
Session.subscribe(session.id)
|
||||
section_id = insert_section(session.pid)
|
||||
cell_id = insert_text_cell(session.pid, section_id, :code, ~s{System.fetch_env!("LB_FOO")})
|
||||
code = ~s{System.fetch_env!("LB_#{secret.name}")}
|
||||
cell_id = insert_text_cell(session.pid, section_id, :code, code)
|
||||
|
||||
Session.queue_cell_evaluation(session.pid, cell_id)
|
||||
assert_receive {:operation, {:add_cell_evaluation_response, _, ^cell_id, _, _}}
|
||||
|
|
@ -1121,8 +1129,80 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
|> has_element?()
|
||||
end
|
||||
|
||||
test "adding an unavailable secret using 'Add secret' button",
|
||||
%{conn: conn, session: session} do
|
||||
secret = build(:secret, name: "MYUNAVAILABLESECRET", value: "123456", origin: :session)
|
||||
Session.subscribe(session.id)
|
||||
section_id = insert_section(session.pid)
|
||||
code = ~s{System.fetch_env!("LB_#{secret.name}")}
|
||||
cell_id = insert_text_cell(session.pid, section_id, :code, code)
|
||||
|
||||
Session.queue_cell_evaluation(session.pid, cell_id)
|
||||
assert_receive {:operation, {:add_cell_evaluation_response, _, ^cell_id, _, _}}
|
||||
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session.id}")
|
||||
|
||||
expected_url = Routes.session_path(conn, :secrets, session.id, secret_name: secret.name)
|
||||
|
||||
add_secret_button = element(view, "a[href='#{expected_url}']")
|
||||
|
||||
assert has_element?(add_secret_button)
|
||||
render_click(add_secret_button)
|
||||
|
||||
secrets_component = with_target(view, "#secrets-modal")
|
||||
form_element = element(secrets_component, "form[phx-submit='save']")
|
||||
|
||||
assert has_element?(form_element)
|
||||
render_submit(form_element, %{data: %{value: secret.value, store: "session"}})
|
||||
|
||||
assert_session_secret(view, session.pid, secret)
|
||||
refute secret in Livebook.Secrets.get_secrets()
|
||||
|
||||
Session.queue_cell_evaluation(session.pid, cell_id)
|
||||
|
||||
assert_receive {:operation,
|
||||
{:add_cell_evaluation_response, _, ^cell_id, {:text, output}, _}}
|
||||
|
||||
assert output == "\e[32m\"#{secret.value}\"\e[0m"
|
||||
end
|
||||
|
||||
test "granting access for unavailable secret using 'Add secret' button",
|
||||
%{conn: conn, session: session} do
|
||||
secret = insert_secret(name: "UNAVAILABLESECRET", value: "123456")
|
||||
Session.subscribe(session.id)
|
||||
section_id = insert_section(session.pid)
|
||||
code = ~s{System.fetch_env!("LB_#{secret.name}")}
|
||||
cell_id = insert_text_cell(session.pid, section_id, :code, code)
|
||||
|
||||
Session.queue_cell_evaluation(session.pid, cell_id)
|
||||
assert_receive {:operation, {:add_cell_evaluation_response, _, ^cell_id, _, _}}
|
||||
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session.id}")
|
||||
|
||||
expected_url = Routes.session_path(conn, :secrets, session.id, secret_name: secret.name)
|
||||
|
||||
add_secret_button = element(view, "a[href='#{expected_url}']")
|
||||
|
||||
assert has_element?(add_secret_button)
|
||||
assert secret in Livebook.Secrets.get_secrets()
|
||||
|
||||
render_click(add_secret_button)
|
||||
secrets_component = with_target(view, "#secrets-modal")
|
||||
grant_access_button = element(secrets_component, "button", "Grant access")
|
||||
render_click(grant_access_button)
|
||||
|
||||
assert_session_secret(view, session.pid, secret)
|
||||
|
||||
Session.queue_cell_evaluation(session.pid, cell_id)
|
||||
|
||||
assert_receive {:operation,
|
||||
{:add_cell_evaluation_response, _, ^cell_id, {:text, output}, _}}
|
||||
|
||||
assert output == "\e[32m\"#{secret.value}\"\e[0m"
|
||||
end
|
||||
|
||||
test "loads secret from temporary storage", %{conn: conn, session: session} do
|
||||
secret = %Secret{name: "FOOBARBAZ", value: "ChonkyCat"}
|
||||
secret = build(:secret, name: "FOOBARBAZ", value: "ChonkyCat", origin: :startup)
|
||||
Livebook.Secrets.set_temporary_secrets([secret])
|
||||
|
||||
{:ok, view, _} = live(conn, "/sessions/#{session.id}")
|
||||
|
|
@ -1279,4 +1359,19 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
{:ok, session} = Sessions.fetch_session(session_id)
|
||||
Session.close(session.pid)
|
||||
end
|
||||
|
||||
defp assert_session_secret(view, session_pid, secret) do
|
||||
selector =
|
||||
case secret do
|
||||
%{name: name, origin: :session} -> "#session-secret-#{name}-title"
|
||||
%{name: name, origin: :app} -> "#app-secret-#{name}-title"
|
||||
%{name: name, origin: {:hub, id}} -> "#hub-#{id}-secret-#{name}-title"
|
||||
end
|
||||
|
||||
assert has_element?(view, selector)
|
||||
secrets = Session.get_data(session_pid).secrets
|
||||
|
||||
assert Map.has_key?(secrets, secret.name)
|
||||
assert secrets[secret.name] == secret.value
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ defmodule Livebook.EnterpriseIntegrationCase do
|
|||
|
||||
@moduletag :enterprise_integration
|
||||
|
||||
import Livebook.EnterpriseIntegrationCase,
|
||||
only: [start_new_instance: 1, stop_new_instance: 1]
|
||||
|
||||
alias Livebook.EnterpriseServer
|
||||
end
|
||||
end
|
||||
|
|
@ -22,4 +25,23 @@ defmodule Livebook.EnterpriseIntegrationCase do
|
|||
{:ok,
|
||||
url: EnterpriseServer.url(), token: EnterpriseServer.token(), user: EnterpriseServer.user()}
|
||||
end
|
||||
|
||||
def start_new_instance(name) do
|
||||
suffix = Ecto.UUID.generate() |> :erlang.phash2() |> to_string()
|
||||
app_port = Enum.random(1000..9000) |> to_string()
|
||||
|
||||
{:ok, _} =
|
||||
EnterpriseServer.start(name,
|
||||
env: %{
|
||||
"DATABASE_URL" =>
|
||||
"postgres://postgres:postgres@localhost:5432/enterprise_integration_#{suffix}"
|
||||
},
|
||||
app_port: app_port
|
||||
)
|
||||
end
|
||||
|
||||
def stop_new_instance(name) do
|
||||
EnterpriseServer.disconnect(name)
|
||||
EnterpriseServer.drop_database(name)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -50,6 +50,14 @@ defmodule Livebook.Factory do
|
|||
}
|
||||
end
|
||||
|
||||
def build(:secret) do
|
||||
%Livebook.Secrets.Secret{
|
||||
name: "FOO",
|
||||
value: "123",
|
||||
origin: :app
|
||||
}
|
||||
end
|
||||
|
||||
def build(factory_name, attrs \\ %{}) do
|
||||
factory_name |> build() |> struct!(attrs)
|
||||
end
|
||||
|
|
@ -64,6 +72,11 @@ defmodule Livebook.Factory do
|
|||
|> Livebook.Hubs.save_hub()
|
||||
end
|
||||
|
||||
def insert_secret(attrs \\ %{}) do
|
||||
secret = build(:secret, attrs)
|
||||
Livebook.Secrets.set_secret(secret)
|
||||
end
|
||||
|
||||
def insert_env_var(factory_name, attrs \\ %{}) do
|
||||
env_var = build(factory_name, attrs)
|
||||
attributes = env_var |> Map.from_struct() |> Map.to_list()
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ defmodule Livebook.EnterpriseServer do
|
|||
end
|
||||
|
||||
def get_node(name \\ @name) do
|
||||
GenServer.call(name, :fetch_node)
|
||||
GenServer.call(name, :fetch_node, @timeout)
|
||||
end
|
||||
|
||||
def drop_database(name \\ @name) do
|
||||
|
|
@ -261,8 +261,8 @@ defmodule Livebook.EnterpriseServer do
|
|||
defp env(app_port, state_env) do
|
||||
env = %{
|
||||
"MIX_ENV" => "livebook",
|
||||
"LIVEBOOK_ENTERPRISE_PORT" => to_string(app_port),
|
||||
"LIVEBOOK_ENTERPRISE_DEBUG" => debug()
|
||||
"PORT" => to_string(app_port),
|
||||
"DEBUG" => debug()
|
||||
}
|
||||
|
||||
if state_env do
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue