Implements :origin field to Secret struct (#1667)

This commit is contained in:
Alexandre de Souza 2023-01-31 19:17:05 -03:00 committed by GitHub
parent eaa4856972
commit d70764517f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 498 additions and 307 deletions

View file

@ -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)

View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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]] ++

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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