Redirect to Hub page to update or delete a secret (#2005)

This commit is contained in:
Alexandre de Souza 2023-06-23 17:54:13 -03:00 committed by GitHub
parent 6519563fb2
commit 05d691d407
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 179 additions and 128 deletions

View file

@ -197,8 +197,7 @@ defmodule Livebook.Application do
%Livebook.Secrets.Secret{
name: name,
value: value,
hub_id: nil,
readonly: true
hub_id: nil
}
end

View file

@ -235,7 +235,7 @@ defmodule Livebook.Hubs do
:ok
| {:error, Ecto.Changeset.t()}
| {:transport_error, String.t()}
def update_secret(hub, %Secret{readonly: false} = secret) do
def update_secret(hub, %Secret{} = secret) do
Provider.update_secret(hub, secret)
end
@ -243,7 +243,7 @@ defmodule Livebook.Hubs do
Deletes a secret for given hub.
"""
@spec delete_secret(Provider.t(), Secret.t()) :: :ok | {:transport_error, String.t()}
def delete_secret(hub, %Secret{readonly: false} = secret) do
def delete_secret(hub, %Secret{} = secret) do
Provider.delete_secret(hub, secret)
end

View file

@ -139,8 +139,7 @@ defmodule Livebook.Hubs.TeamClient do
%Livebook.Secrets.Secret{
name: name,
value: decrypted_value,
hub_id: state.hub.id,
readonly: true
hub_id: state.hub.id
}
end

View file

@ -32,8 +32,7 @@ defmodule Livebook.Migration do
secret = %Livebook.Secrets.Secret{
name: name,
value: value,
hub_id: Livebook.Hubs.Personal.id(),
readonly: false
hub_id: Livebook.Hubs.Personal.id()
}
Livebook.Secrets.set_secret(secret)

View file

@ -75,11 +75,7 @@ defmodule Livebook.Secrets do
"""
@spec set_secret(Secret.t()) :: Secret.t()
def set_secret(secret) do
attributes =
secret
|> Map.from_struct()
|> Map.delete(:readonly)
attributes = Map.from_struct(secret)
:ok = Storage.insert(@namespace, secret.name, Map.to_list(attributes))
secret
@ -101,8 +97,7 @@ defmodule Livebook.Secrets do
%Secret{
name: name,
value: value,
hub_id: fields[:hub_id] || Livebook.Hubs.Personal.id(),
readonly: false
hub_id: fields[:hub_id] || Livebook.Hubs.Personal.id()
}
end

View file

@ -6,15 +6,13 @@ defmodule Livebook.Secrets.Secret do
@type t :: %__MODULE__{
name: String.t() | nil,
value: String.t() | nil,
hub_id: String.t() | nil,
readonly: boolean() | nil
hub_id: String.t() | nil
}
@primary_key {:name, :string, autogenerate: false}
embedded_schema do
field :value, :string
field :hub_id, :string
field :readonly, :boolean, virtual: true, default: false
end
def changeset(secret, attrs \\ %{}) do

View file

@ -156,8 +156,8 @@ defmodule LivebookWeb.Hub.Edit.TeamComponent do
phx-click={
JS.remove_attribute("type", to: "#teams-key-toggle input")
|> JS.set_attribute({"type", "text"}, to: "#teams-key-toggle input")
|> JS.add_class("hidden", to: "#teams-key-toggle [data-show]")
|> JS.remove_class("hidden", to: "#teams-key-toggle [data-hide]")
|> toggle_class("hidden", to: "#teams-key-toggle [data-show]")
|> toggle_class("hidden", to: "#teams-key-toggle [data-hide]")
}
>
<.remix_icon icon="eye-line" class="text-xl" />
@ -170,8 +170,8 @@ defmodule LivebookWeb.Hub.Edit.TeamComponent do
phx-click={
JS.remove_attribute("type", to: "#teams-key-toggle input")
|> JS.set_attribute({"type", "password"}, to: "#teams-key-toggle input")
|> JS.remove_class("hidden", to: "#teams-key-toggle [data-show]")
|> JS.add_class("hidden", to: "#teams-key-toggle [data-hide]")
|> toggle_class("hidden", to: "#teams-key-toggle [data-show]")
|> toggle_class("hidden", to: "#teams-key-toggle [data-hide]")
}
>
<.remix_icon icon="eye-off-line" class="text-xl" />

View file

@ -15,7 +15,7 @@ defmodule LivebookWeb.Hub.SecretFormComponent do
socket = assign(socket, assigns)
{:ok, assign(socket, title: title(socket), changeset: changeset)}
{:ok, assign(socket, title: title(socket), button: button(socket), changeset: changeset)}
end
@impl true
@ -57,8 +57,8 @@ defmodule LivebookWeb.Hub.SecretFormComponent do
<.hidden_field field={f[:hub_id]} value={@hub.id} />
<div class="flex space-x-2">
<button class="button-base button-blue" type="submit" disabled={not @changeset.valid?}>
<.remix_icon icon="add-line" class="align-middle" />
<span class="font-normal">Add</span>
<.remix_icon icon={@button.icon} class="align-middle" />
<span class="font-normal"><%= @button.label %></span>
</button>
<.link patch={@return_to} class="button-base button-outlined-gray">
Cancel
@ -108,6 +108,9 @@ defmodule LivebookWeb.Hub.SecretFormComponent do
defp title(%{assigns: %{secret_name: nil}}), do: "Add secret"
defp title(_), do: "Edit secret"
defp button(%{assigns: %{secret_name: nil}}), do: %{icon: "add-line", label: "Add"}
defp button(_), do: %{icon: "save-line", label: "Save"}
defp set_secret(%{assigns: %{secret_name: nil}} = socket, %Secret{} = secret) do
Hubs.create_secret(socket.assigns.hub, secret)
end

View file

@ -245,7 +245,7 @@ defmodule LivebookWeb.LayoutHelpers do
[
id: "hub-#{hub.id}",
navigate: to,
data_tooltip: Provider.connection_error(hub.provider),
"data-tooltip": Provider.connection_error(hub.provider),
class: "tooltip right " <> class
]
end

View file

@ -20,7 +20,7 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do
socket =
socket
|> assign_new(:changeset, fn ->
attrs = %{name: secret_name, value: nil, hub_id: nil, readonly: false}
attrs = %{name: secret_name, value: nil, hub_id: nil}
Secrets.change_secret(%Secret{}, attrs)
end)
|> assign_new(:grant_access_secret, fn ->

View file

@ -1,9 +1,6 @@
defmodule LivebookWeb.SessionLive.SecretsListComponent do
use LivebookWeb, :live_component
alias Livebook.Hubs
alias Livebook.Secrets
alias Livebook.Secrets.Secret
alias Livebook.Session
@impl true
@ -19,44 +16,14 @@ defmodule LivebookWeb.SessionLive.SecretsListComponent 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">
<div
<.session_secret
:for={
secret <- @secrets |> Session.Data.session_secrets(@hub.id) |> Enum.sort_by(& &1.name)
}
class="flex flex-col text-gray-500 rounded-lg px-2 pt-1"
id={"session-secret-#{secret.name}"}
>
<span
class="text-sm font-mono break-all flex-row cursor-pointer"
phx-click={
JS.toggle(to: "#session-secret-#{secret.name}-detail", display: "flex")
|> toggle_class("bg-gray-100", to: "#session-secret-#{secret.name}")
}
>
<%= secret.name %>
</span>
<div
class="flex flex-row justify-between items-center my-1 hidden"
id={"session-secret-#{secret.name}-detail"}
>
<span class="text-sm font-mono break-all flex-row">
<%= secret.value %>
</span>
<button
id={"session-secret-#{secret.name}-delete"}
type="button"
phx-click={
JS.push("delete_session_secret",
value: %{secret_name: secret.name},
target: @myself
)
}
class="hover:text-gray-900"
>
<.remix_icon icon="delete-bin-line" />
</button>
</div>
</div>
secret={secret}
myself={@myself}
/>
</div>
<.link
@ -83,7 +50,7 @@ defmodule LivebookWeb.SessionLive.SecretsListComponent do
</div>
<div class="flex flex-col space-y-4 mt-6">
<.secrets_item
<.hub_secret
:for={secret <- Enum.sort_by(@hub_secrets, & &1.name)}
id={"hub-#{secret.hub_id}-secret-#{secret.name}"}
secret={secret}
@ -97,9 +64,46 @@ defmodule LivebookWeb.SessionLive.SecretsListComponent do
"""
end
defp secrets_item(assigns) do
defp session_secret(assigns) do
~H"""
<div class="flex flex-col text-gray-500 rounded-lg px-2 pt-1" id={@id}>
<div id={@id} class="flex flex-col text-gray-500 rounded-lg px-2 pt-1">
<span
class="text-sm font-mono break-all flex-row cursor-pointer"
phx-click={
JS.toggle(to: "#session-secret-#{@secret.name}-detail", display: "flex")
|> toggle_class("bg-gray-100", to: "#session-secret-#{@secret.name}")
}
>
<%= @secret.name %>
</span>
<div
class="flex-row justify-between items-center my-1 hidden"
id={"session-secret-#{@secret.name}-detail"}
>
<span class="text-sm font-mono break-all flex-row tooltip right" data-tooltip={@secret.value}>
*****
</span>
<button
id={"session-secret-#{@secret.name}-delete"}
type="button"
phx-click={
JS.push("delete_session_secret",
value: %{secret_name: @secret.name},
target: @myself
)
}
class="hover:text-gray-900"
>
<.remix_icon icon="delete-bin-line" />
</button>
</div>
</div>
"""
end
defp hub_secret(assigns) do
~H"""
<div id={@id} class="flex flex-col text-gray-500 rounded-lg px-2 pt-1">
<div class="flex flex-col text-gray-800">
<div class="flex flex-col">
<div class="flex justify-between items-center">
@ -144,28 +148,21 @@ defmodule LivebookWeb.SessionLive.SecretsListComponent do
<.hidden_field field={f[:name]} value={@secret.name} />
</.form>
</div>
<div class="flex flex-row justify-between items-center my-1 hidden" id={"#{@id}-detail"}>
<span class="text-sm font-mono break-all flex-row">
<%= Session.Data.secret_used_value(@secret, @secrets) %>
</span>
<button
:if={!@secret.readonly}
id={"#{@id}-delete"}
type="button"
phx-click={
JS.push("delete_hub_secret",
value: %{
name: @secret.name,
value: @secret.value,
hub_id: @secret.hub_id
},
target: @myself
)
}
class="hover:text-gray-900"
<div class="flex-row justify-between items-center my-1 hidden" id={"#{@id}-detail"}>
<span
class="text-sm font-mono break-all flex-row tooltip right"
data-tooltip={@secret.value}
>
<.remix_icon icon="delete-bin-line" />
</button>
*****
</span>
<.link
id="edit-secret-button"
navigate={~p"/hub/#{@secret.hub_id}/secrets/edit/#{@secret.name}"}
class="hover:text-gray-900"
role="button"
>
<.remix_icon icon="pencil-line" />
</.link>
</div>
</div>
</div>
@ -224,28 +221,4 @@ defmodule LivebookWeb.SessionLive.SecretsListComponent do
confirm_icon: "delete-bin-6-line"
)}
end
def handle_event("delete_hub_secret", attrs, socket) do
%{hub: hub, session: session} = socket.assigns
on_confirm = fn socket ->
{:ok, secret} = Secrets.update_secret(%Secret{}, attrs)
with :ok <- Hubs.delete_secret(hub, secret),
:ok <- Session.unset_secret(session.pid, secret.name) do
socket
else
{:transport_error, reason} ->
put_flash(socket, :error, reason)
end
end
{:noreply,
confirm(socket, on_confirm,
title: "Delete hub secret - #{attrs["name"]}",
description: "Are you sure you want to delete this hub secret?",
confirm_text: "Delete",
confirm_icon: "delete-bin-6-line"
)}
end
end

View file

@ -40,7 +40,6 @@ defmodule Livebook.SecretsTest do
assert attrs.name == secret.name
assert attrs.value == secret.value
assert attrs.hub_id == secret.hub_id
refute secret.readonly
end
test "returns changeset error" do

View file

@ -67,7 +67,7 @@ defmodule LivebookWeb.Integration.Hub.EditLiveTest do
test "creates a secret", %{conn: conn, hub: hub} do
{:ok, view, _html} = live(conn, ~p"/hub/#{hub.id}")
secret = build(:secret, name: "TEAM_ADD_SECRET", hub_id: hub.id, readonly: true)
secret = build(:secret, name: "TEAM_ADD_SECRET", hub_id: hub.id)
attrs = %{
secret: %{
@ -108,7 +108,8 @@ defmodule LivebookWeb.Integration.Hub.EditLiveTest do
end
test "updates existing secret", %{conn: conn, hub: hub} do
secret = insert_secret(name: "TEAM_EDIT_SECRET", hub_id: hub.id, readonly: true)
secret = insert_secret(name: "TEAM_EDIT_SECRET", hub_id: hub.id)
assert_receive {:secret_created, ^secret}
{:ok, view, _html} = live(conn, ~p"/hub/#{hub.id}")
@ -152,7 +153,8 @@ defmodule LivebookWeb.Integration.Hub.EditLiveTest do
end
test "deletes existing secret", %{conn: conn, hub: hub} do
secret = insert_secret(name: "TEAM_DELETE_SECRET", hub_id: hub.id, readonly: true)
secret = insert_secret(name: "TEAM_DELETE_SECRET", hub_id: hub.id)
assert_receive {:secret_created, ^secret}
{:ok, view, _html} = live(conn, ~p"/hub/#{hub.id}")

View file

@ -1,9 +1,9 @@
defmodule LivebookWeb.Integration.SessionLiveTest do
use Livebook.TeamsIntegrationCase, async: true
import Phoenix.LiveViewTest
import Livebook.HubHelpers
import Livebook.SessionHelpers
import Phoenix.LiveViewTest
alias Livebook.{Sessions, Session}
@ -64,8 +64,7 @@ defmodule LivebookWeb.Integration.SessionLiveTest do
build(:secret,
name: "BIG_IMPORTANT_SECRET",
value: "123",
hub_id: team.id,
readonly: true
hub_id: team.id
)
attrs = %{
@ -92,6 +91,44 @@ defmodule LivebookWeb.Integration.SessionLiveTest do
assert_session_secret(view, session.pid, secret, :hub_secrets)
end
test "redirects the user to update or delete a secret",
%{conn: conn, user: user, node: node, session: session} do
Livebook.Hubs.subscribe([:secrets, :connection])
team = create_team_hub(user, node)
id = team.id
assert_receive {:hub_connected, ^id}
Session.subscribe(session.id)
# creates a secret
secret_name = "BIG_IMPORTANT_SECRET_TO_BE_UPDATED_OR_DELETED"
secret_value = "123"
insert_secret(
name: secret_name,
value: secret_value,
hub_id: team.id
)
assert_receive {:secret_created, %{name: ^secret_name, value: ^secret_value}}
# selects the notebook's hub with team hub id
Session.set_notebook_hub(session.pid, team.id)
# loads the session page
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
# clicks the button to edit a secret
view
|> with_target("#secrets_list")
|> element("#hub-#{id}-secret-#{secret_name}-detail #edit-secret-button")
|> render_click()
# redirects to hub page and loads the modal with
# the secret name and value filled
assert_redirect(view, ~p"/hub/#{id}/secrets/edit/#{secret_name}")
end
test "toggle a secret from team hub", %{conn: conn, session: session, user: user, node: node} do
team = create_team_hub(user, node)
Session.subscribe(session.id)
@ -107,8 +144,7 @@ defmodule LivebookWeb.Integration.SessionLiveTest do
build(:secret,
name: "POSTGRES_PASSWORD",
value: "123456789",
hub_id: team.id,
readonly: true
hub_id: team.id
)
assert Livebook.Teams.create_secret(team, secret) == :ok
@ -130,8 +166,7 @@ defmodule LivebookWeb.Integration.SessionLiveTest do
build(:secret,
name: "MYSQL_PASS",
value: "admin",
hub_id: team.id,
readonly: true
hub_id: team.id
)
# selects the notebook's hub with team hub id
@ -186,8 +221,7 @@ defmodule LivebookWeb.Integration.SessionLiveTest do
build(:secret,
name: "PGPASS",
value: "admin",
hub_id: team.id,
readonly: true
hub_id: team.id
)
# selects the notebook's hub with team hub id

View file

@ -1101,7 +1101,7 @@ defmodule LivebookWeb.SessionLiveTest do
describe "secrets" do
setup do
{:ok, hub: build(:personal)}
{:ok, hub: Livebook.Hubs.fetch_hub!(Livebook.Hubs.Personal.id())}
end
test "adds a secret from form", %{conn: conn, session: session} do
@ -1300,6 +1300,57 @@ defmodule LivebookWeb.SessionLiveTest do
assert_session_secret(view, session.pid, updated_hub_secret)
end
test "redirects the user to update or delete a secret",
%{conn: conn, session: session, hub: hub} do
Session.subscribe(session.id)
# creates a secret
secret_name = "SECRET_TO_BE_UPDATED_OR_DELETED"
secret_value = "123"
insert_secret(name: secret_name, value: secret_value)
# receives the operation event
assert_receive {:operation, {:sync_hub_secrets, "__server__"}}
# selects the notebook's hub with team hub id
Session.set_notebook_hub(session.pid, hub.id)
# loads the session page
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
# clicks the button to edit a secret
view
|> with_target("#secrets_list")
|> element("#hub-#{hub.id}-secret-#{secret_name}-detail #edit-secret-button")
|> render_click()
# redirects to hub page and loads the modal with
# the secret name and value filled
assert_redirect(view, ~p"/hub/#{hub.id}/secrets/edit/#{secret_name}")
{:ok, view, _} = live(conn, ~p"/hub/#{hub.id}/secrets/edit/#{secret_name}")
assert render(view) =~ "Edit secret"
# fills and submits the secrets modal form
# to update the secret on team hub page
secret_new_value = "123456"
attrs = %{secret: %{name: secret_name, value: secret_new_value}}
form = element(view, "#secrets-form")
render_change(form, attrs)
render_submit(form, attrs)
# receives the operation event
assert_receive {:operation, {:sync_hub_secrets, "__server__"}}
# validates the secret
secrets = Livebook.Hubs.get_secrets(hub)
hub_secret = Enum.find(secrets, &(&1.name == secret_name))
assert hub_secret.value == secret_new_value
refute hub_secret.value == secret_value
end
end
describe "environment variables" do

View file

@ -51,8 +51,7 @@ defmodule Livebook.Factory do
%Livebook.Secrets.Secret{
name: "FOO",
value: "123",
hub_id: Livebook.Hubs.Personal.id(),
readonly: false
hub_id: Livebook.Hubs.Personal.id()
}
end