diff --git a/lib/livebook/application.ex b/lib/livebook/application.ex index 5bf9954ed..7a05faa6b 100644 --- a/lib/livebook/application.ex +++ b/lib/livebook/application.ex @@ -190,7 +190,7 @@ defmodule Livebook.Application do %Livebook.Secrets.Secret{name: name, value: value, origin: :startup} end - Livebook.Secrets.set_temporary_secrets(secrets) + Livebook.Secrets.set_startup_secrets(secrets) end defp config_env_var?("LIVEBOOK_" <> _), do: true diff --git a/lib/livebook/hubs.ex b/lib/livebook/hubs.ex index ca0a9eb7b..c3456cec7 100644 --- a/lib/livebook/hubs.ex +++ b/lib/livebook/hubs.ex @@ -3,7 +3,6 @@ defmodule Livebook.Hubs do alias Livebook.Storage alias Livebook.Hubs.{Broadcasts, Enterprise, Fly, Metadata, Personal, Provider} - alias Livebook.Secrets alias Livebook.Secrets.Secret @namespace :hubs @@ -181,9 +180,7 @@ defmodule Livebook.Hubs do """ @spec connect_hubs() :: :ok def connect_hubs do - for hub <- get_hubs(), - capability?(hub, [:connect]), - do: connect_hub(hub) + for hub <- get_hubs([:connect]), do: connect_hub(hub) :ok end @@ -211,19 +208,34 @@ defmodule Livebook.Hubs do @doc """ Creates a secret for given hub. """ - @spec create_secret(Secret.t()) :: :ok | {:error, list({String.t(), list(String.t())})} + @spec create_secret(Secret.t()) :: :ok | {:error, list({atom(), list(String.t())})} def create_secret(%Secret{origin: {:hub, id}} = secret) do - case get_hub(id) do - {:ok, hub} -> - if capability?(hub, [:secrets]) do - Provider.create_secret(hub, secret) - else - {:error, Secrets.add_secret_error(secret, :origin, "is invalid")} - end + {:ok, hub} = get_hub(id) + true = capability?(hub, [:create_secret]) - :error -> - {:error, Secrets.add_secret_error(secret, :origin, "is invalid")} - end + Provider.create_secret(hub, secret) + end + + @doc """ + Updates a secret for given hub. + """ + @spec update_secret(Secret.t()) :: :ok | {:error, list({atom(), list(String.t())})} + def update_secret(%Secret{origin: {:hub, id}} = secret) do + {:ok, hub} = get_hub(id) + true = capability?(hub, [:update_secret]) + + Provider.update_secret(hub, secret) + end + + @doc """ + Deletes a secret for given hub. + """ + @spec delete_secret(Secret.t()) :: :ok | {:error, list({atom(), list(String.t())})} + def delete_secret(%Secret{origin: {:hub, id}} = secret) do + {:ok, hub} = get_hub(id) + true = capability?(hub, [:delete_secret]) + + Provider.delete_secret(hub, secret) end defp capability?(hub, capabilities) do diff --git a/lib/livebook/hubs/broadcasts.ex b/lib/livebook/hubs/broadcasts.ex index ad46d3eb4..aab834c04 100644 --- a/lib/livebook/hubs/broadcasts.ex +++ b/lib/livebook/hubs/broadcasts.ex @@ -10,7 +10,7 @@ defmodule Livebook.Hubs.Broadcasts do @secrets_topic "hubs:secrets" @doc """ - Broadcasts when hubs changed under `hubs:crud` topic + Broadcasts under `hubs:crud` topic when hubs changed. """ @spec hub_changed() :: broadcast() def hub_changed do @@ -18,7 +18,7 @@ defmodule Livebook.Hubs.Broadcasts do end @doc """ - Broadcasts when hub connected under `hubs:connection` topic + Broadcasts under `hubs:connection` topic when hub connected. """ @spec hub_connected() :: broadcast() def hub_connected do @@ -26,7 +26,7 @@ defmodule Livebook.Hubs.Broadcasts do end @doc """ - Broadcasts when hub disconnected under `hubs:connection` topic + Broadcasts under `hubs:connection` topic when hub disconnected. """ @spec hub_disconnected() :: broadcast() def hub_disconnected do @@ -34,7 +34,7 @@ defmodule Livebook.Hubs.Broadcasts do end @doc """ - Broadcasts when hub had an error when connecting under `hubs:connection` topic + Broadcasts under `hubs:connection` topic when hub received a connection error. """ @spec hub_connection_failed(String.t()) :: broadcast() def hub_connection_failed(reason) when is_binary(reason) do @@ -42,15 +42,7 @@ defmodule Livebook.Hubs.Broadcasts do end @doc """ - Broadcasts when hub had an error when disconnecting under `hubs:connection` topic - """ - @spec hub_disconnection_failed(String.t()) :: broadcast() - def hub_disconnection_failed(reason) when is_binary(reason) do - broadcast(@connection_topic, {:hub_disconnection_failed, reason}) - end - - @doc """ - Broadcasts when hub received a new secret under `hubs:secrets` topic + Broadcasts under `hubs:secrets` topic when hub received a new secret. """ @spec secret_created(Secret.t()) :: broadcast() def secret_created(%Secret{} = secret) do @@ -58,13 +50,21 @@ defmodule Livebook.Hubs.Broadcasts do end @doc """ - Broadcasts when hub received an updated secret under `hubs:secrets` topic + Broadcasts under `hubs:secrets` topic when hub received an updated secret. """ @spec secret_updated(Secret.t()) :: broadcast() def secret_updated(%Secret{} = secret) do broadcast(@secrets_topic, {:secret_updated, secret}) end + @doc """ + Broadcasts under `hubs:secrets` topic when hub received a deleted secret. + """ + @spec secret_deleted(Secret.t()) :: broadcast() + def secret_deleted(%Secret{} = secret) do + broadcast(@secrets_topic, {:secret_deleted, secret}) + end + defp broadcast(topic, message) do Phoenix.PubSub.broadcast(Livebook.PubSub, topic, message) end diff --git a/lib/livebook/hubs/enterprise.ex b/lib/livebook/hubs/enterprise.ex index 581a05f86..eb2583c03 100644 --- a/lib/livebook/hubs/enterprise.ex +++ b/lib/livebook/hubs/enterprise.ex @@ -145,17 +145,16 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Enterprise do EnterpriseClient.stop(enterprise.id) end - def capabilities(_enterprise), do: [:connect, :secrets] + def capabilities(_enterprise), do: ~w(connect secrets list_secrets create_secret)a def get_secrets(enterprise) do EnterpriseClient.get_secrets(enterprise.id) end def create_secret(enterprise, secret) do - create_secret_request = - LivebookProto.CreateSecretRequest.new!(name: secret.name, value: secret.value) + data = LivebookProto.build_create_secret_request(name: secret.name, value: secret.value) - case EnterpriseClient.send_request(enterprise.id, create_secret_request) do + case EnterpriseClient.send_request(enterprise.id, data) do {:create_secret, _} -> :ok @@ -178,6 +177,10 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Enterprise do end end + def update_secret(_enterprise, _secret), do: raise("not implemented") + + def delete_secret(_enterprise, _secret), do: raise("not implemented") + def connection_error(enterprise) do reason = EnterpriseClient.get_connection_error(enterprise.id) "Cannot connect to Hub: #{reason}. Will attempt to reconnect automatically..." diff --git a/lib/livebook/hubs/enterprise_client.ex b/lib/livebook/hubs/enterprise_client.ex index 4216516d4..e0fc793fe 100644 --- a/lib/livebook/hubs/enterprise_client.ex +++ b/lib/livebook/hubs/enterprise_client.ex @@ -1,6 +1,7 @@ defmodule Livebook.Hubs.EnterpriseClient do @moduledoc false use GenServer + require Logger alias Livebook.Hubs.Broadcasts alias Livebook.Hubs.Enterprise @@ -86,6 +87,14 @@ defmodule Livebook.Hubs.EnterpriseClient do {:ok, %__MODULE__{hub: enterprise, server: pid}} end + @impl true + def handle_continue(:synchronize_user, state) do + data = LivebookProto.build_handshake_request(app_version: Livebook.Config.app_version()) + {:handshake, _} = ClientConnection.send_request(state.server, data) + + {:noreply, state} + end + @impl true def handle_call(:fetch_server, _caller, state) do if state.connected? do @@ -110,7 +119,7 @@ defmodule Livebook.Hubs.EnterpriseClient do @impl true def handle_info({:connect, :ok, _}, state) do Broadcasts.hub_connected() - {:noreply, %{state | connected?: true, connection_error: nil}} + {:noreply, %{state | connected?: true, connection_error: nil}, {:continue, :synchronize_user}} end def handle_info({:connect, :error, reason}, state) do @@ -118,25 +127,17 @@ defmodule Livebook.Hubs.EnterpriseClient do {:noreply, %{state | connected?: false, connection_error: reason}} end - def handle_info({:event, :secret_created, %{name: name, value: value}}, state) do - 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, origin: {:hub, state.hub.id}} - Broadcasts.secret_updated(secret) - - {:noreply, put_secret(state, secret)} - end - def handle_info({:disconnect, :ok, :disconnected}, state) do Broadcasts.hub_disconnected() {:stop, :normal, state} end + def handle_info({:event, topic, data}, state) do + Logger.debug("Received event #{topic} with data: #{inspect(data)}") + + {:noreply, handle_event(topic, data, state)} + end + # Private defp registry_name(id) do @@ -144,6 +145,79 @@ defmodule Livebook.Hubs.EnterpriseClient do end defp put_secret(state, secret) do - %{state | secrets: [secret | Enum.reject(state.secrets, &(&1.name == secret.name))]} + state = remove_secret(state, secret) + %{state | secrets: [secret | state.secrets]} + end + + defp remove_secret(state, secret) do + %{state | secrets: Enum.reject(state.secrets, &(&1.name == secret.name))} + end + + defp build_secret(state, %{name: name, value: value}), + do: %Secret{name: name, value: value, origin: {:hub, state.hub.id}} + + defp update_hub(state, name) do + case Enterprise.update_hub(state.hub, %{hub_name: name}) do + {:ok, hub} -> %{state | hub: hub} + {:error, _} -> state + end + end + + defp handle_event(:secret_created, secret_created, state) do + secret = build_secret(state, secret_created) + Broadcasts.secret_created(secret) + + put_secret(state, secret) + end + + defp handle_event(:secret_updated, secret_updated, state) do + secret = build_secret(state, secret_updated) + Broadcasts.secret_updated(secret) + + put_secret(state, secret) + end + + defp handle_event(:secret_deleted, secret_deleted, state) do + secret = build_secret(state, secret_deleted) + Broadcasts.secret_deleted(secret) + + remove_secret(state, secret) + end + + defp handle_event(:user_synchronized, user_synchronized, %{secrets: []} = state) do + state = update_hub(state, user_synchronized.name) + secrets = for secret <- user_synchronized.secrets, do: build_secret(state, secret) + + %{state | secrets: secrets} + end + + defp handle_event(:user_synchronized, user_synchronized, state) do + state = update_hub(state, user_synchronized.name) + secrets = for secret <- user_synchronized.secrets, do: build_secret(state, secret) + + created_secrets = + Enum.reject(secrets, fn secret -> + Enum.find(state.secrets, &(&1.name == secret.name and &1.value == secret.value)) + end) + + deleted_secrets = + Enum.reject(state.secrets, fn secret -> + Enum.find(secrets, &(&1.name == secret.name)) + end) + + updated_secrets = + Enum.filter(secrets, fn secret -> + Enum.find(state.secrets, &(&1.name == secret.name and &1.value != secret.value)) + end) + + events_by_type = [ + secret_deleted: deleted_secrets, + secret_created: created_secrets, + secret_updated: updated_secrets + ] + + for {type, events} <- events_by_type, event <- events, reduce: state do + state -> handle_event(type, event, state) + end end end diff --git a/lib/livebook/hubs/fly.ex b/lib/livebook/hubs/fly.ex index a78097dea..fcc7c1f78 100644 --- a/lib/livebook/hubs/fly.ex +++ b/lib/livebook/hubs/fly.ex @@ -146,13 +146,20 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Fly do def connection_spec(_fly), do: nil - def disconnect(_fly), do: :ok + def disconnect(_fly), do: raise("not implemented") def capabilities(_fly), do: [] def get_secrets(_fly), do: [] + # TODO: Implement the FlyClient.set_secrets/2 def create_secret(_fly, _secret), do: :ok - def connection_error(_fly), do: nil + # TODO: Implement the FlyClient.set_secrets/2 + def update_secret(_fly, _secret), do: :ok + + # TODO: Implement the FlyClient.unset_secrets/2 + def delete_secret(_fly, _secret), do: :ok + + def connection_error(_fly), do: raise("not implemented") end diff --git a/lib/livebook/hubs/personal.ex b/lib/livebook/hubs/personal.ex index 075407505..988cf9252 100644 --- a/lib/livebook/hubs/personal.ex +++ b/lib/livebook/hubs/personal.ex @@ -62,6 +62,9 @@ defmodule Livebook.Hubs.Personal do end defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Personal do + alias Livebook.Hubs.Broadcasts + alias Livebook.Secrets + def load(personal, fields) do %{personal | id: fields.id, hub_name: fields.hub_name, hub_emoji: fields.hub_emoji} end @@ -80,13 +83,28 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Personal do def connection_spec(_personal), do: nil - def disconnect(_personal), do: :ok + def disconnect(_personal), do: raise("not implemented") - def capabilities(_personal), do: [] + def capabilities(_personal), do: ~w(list_secrets create_secret update_secret delete_secret)a - def get_secrets(_personal), do: [] + def get_secrets(_personal) do + Secrets.get_secrets() + end - def create_secret(_personal, _secret), do: :ok + def create_secret(_personal, secret) do + Secrets.set_secret(secret) + :ok = Broadcasts.secret_created(secret) + end - def connection_error(_personal), do: nil + def update_secret(_personal, secret) do + Secrets.set_secret(secret) + :ok = Broadcasts.secret_updated(secret) + end + + def delete_secret(_personal, secret) do + :ok = Secrets.unset_secret(secret.name) + :ok = Broadcasts.secret_deleted(secret) + end + + def connection_error(_personal), do: raise("not implemented") end diff --git a/lib/livebook/hubs/provider.ex b/lib/livebook/hubs/provider.ex index ee09fb61e..cbaf67314 100644 --- a/lib/livebook/hubs/provider.ex +++ b/lib/livebook/hubs/provider.ex @@ -3,8 +3,9 @@ defprotocol Livebook.Hubs.Provider do alias Livebook.Secrets.Secret - @type t :: Livebook.Hubs.Enterprise.t() | Livebook.Hubs.Fly.t() | Livebook.Hubs.Local.t() - @type capability :: :connect | :secrets + @type t :: Livebook.Hubs.Enterprise.t() | Livebook.Hubs.Fly.t() | Livebook.Hubs.Personal.t() + @type capability :: + :connect | :secrets | :list_secrets | :create_secret | :update_secret | :delete_secret @type capabilities :: list(capability()) @type changeset_errors :: %{required(:errors) => list({String.t(), {Stirng.t(), list()}})} @@ -51,11 +52,23 @@ defprotocol Livebook.Hubs.Provider do def get_secrets(hub) @doc """ - Creates a secret of the given hub. + Creates a secret of the given hub. """ @spec create_secret(t(), Secret.t()) :: :ok | {:error, changeset_errors()} def create_secret(hub, secret) + @doc """ + Updates a secret of the given hub. + """ + @spec update_secret(t(), Secret.t()) :: :ok | {:error, changeset_errors()} + def update_secret(hub, secret) + + @doc """ + Deletes a secret of the given hub. + """ + @spec delete_secret(t(), Secret.t()) :: :ok | {:error, changeset_errors()} + def delete_secret(hub, secret) + @doc """ Gets the connection error from hub. """ diff --git a/lib/livebook/secrets.ex b/lib/livebook/secrets.ex index b7af82c19..96d57ba5e 100644 --- a/lib/livebook/secrets.ex +++ b/lib/livebook/secrets.ex @@ -4,19 +4,17 @@ defmodule Livebook.Secrets do alias Livebook.Storage alias Livebook.Secrets.Secret - @temporary_key :livebook_temporary_secrets + @secret_startup_key :livebook_startup_secrets @doc """ Get the secrets list from storage. """ @spec get_secrets() :: list(Secret.t()) def get_secrets do - temporary_secrets = :persistent_term.get(@temporary_key, []) + startup_secrets = :persistent_term.get(@secret_startup_key, []) + storage_secrets = for fields <- Storage.all(:secrets), do: to_struct(fields) - for fields <- Storage.all(:secrets) do - to_struct(fields) - end - |> Enum.concat(temporary_secrets) + Enum.concat(storage_secrets, startup_secrets) end @doc """ @@ -109,9 +107,9 @@ defmodule Livebook.Secrets do @doc """ Sets additional secrets that are kept only in memory. """ - @spec set_temporary_secrets(list(Secret.t())) :: :ok - def set_temporary_secrets(secrets) do - :persistent_term.put(@temporary_key, secrets) + @spec set_startup_secrets(list(Secret.t())) :: :ok + def set_startup_secrets(secrets) do + :persistent_term.put(@secret_startup_key, secrets) end @doc """ diff --git a/lib/livebook_web/live/hub/edit/enterprise_component.ex b/lib/livebook_web/live/hub/edit/enterprise_component.ex index 8e4ccb711..1293a9cae 100644 --- a/lib/livebook_web/live/hub/edit/enterprise_component.ex +++ b/lib/livebook_web/live/hub/edit/enterprise_component.ex @@ -31,7 +31,6 @@ defmodule LivebookWeb.Hub.Edit.EnterpriseComponent do phx-submit="save" phx-change="validate" phx-target={@myself} - phx-debounce="blur" >
<.emoji_field field={f[:hub_emoji]} label="Emoji" /> diff --git a/lib/livebook_web/live/hub/edit/fly_component.ex b/lib/livebook_web/live/hub/edit/fly_component.ex index b87fa202a..397c1ac3a 100644 --- a/lib/livebook_web/live/hub/edit/fly_component.ex +++ b/lib/livebook_web/live/hub/edit/fly_component.ex @@ -67,7 +67,6 @@ defmodule LivebookWeb.Hub.Edit.FlyComponent do phx-submit="save" phx-change="validate" phx-target={@myself} - phx-debounce="blur" >
<.text_field field={f[:hub_name]} label="Name" /> diff --git a/lib/livebook_web/live/hub/edit/personal_component.ex b/lib/livebook_web/live/hub/edit/personal_component.ex index c04307a0b..d0c646651 100644 --- a/lib/livebook_web/live/hub/edit/personal_component.ex +++ b/lib/livebook_web/live/hub/edit/personal_component.ex @@ -31,7 +31,6 @@ defmodule LivebookWeb.Hub.Edit.PersonalComponent do phx-submit="save" phx-change="validate" phx-target={@myself} - phx-debounce="blur" >
<.text_field field={f[:hub_name]} label="Name" /> diff --git a/lib/livebook_web/live/hub/new/enterprise_component.ex b/lib/livebook_web/live/hub/new/enterprise_component.ex index 8976c0062..f52006ec8 100644 --- a/lib/livebook_web/live/hub/new/enterprise_component.ex +++ b/lib/livebook_web/live/hub/new/enterprise_component.ex @@ -117,12 +117,11 @@ defmodule LivebookWeb.Hub.New.EnterpriseComponent do |> push_patch(to: ~p"/hub")} :hub_connected -> - session_request = - LivebookProto.SessionRequest.new!(app_version: Livebook.Config.app_version()) + data = LivebookProto.build_handshake_request(app_version: Livebook.Config.app_version()) - case EnterpriseClient.send_request(pid, session_request) do - {:session, session_response} -> - base = %{base | external_id: session_response.id} + case EnterpriseClient.send_request(pid, data) do + {:handshake, handshake_response} -> + base = %{base | external_id: handshake_response.id} changeset = Enterprise.validate_hub(base) {:noreply, assign(socket, pid: pid, changeset: changeset, base: base)} diff --git a/lib/livebook_web/live/hub/new/fly_component.ex b/lib/livebook_web/live/hub/new/fly_component.ex index cd8d58435..b1dadefbf 100644 --- a/lib/livebook_web/live/hub/new/fly_component.ex +++ b/lib/livebook_web/live/hub/new/fly_component.ex @@ -31,7 +31,6 @@ defmodule LivebookWeb.Hub.New.FlyComponent do phx-submit="save" phx-change="validate" phx-target={@myself} - phx-debounce="blur" > <.password_field type="password" diff --git a/lib/livebook_web/live/session_live/secrets_component.ex b/lib/livebook_web/live/session_live/secrets_component.ex index 00a0a61c6..f814f2c6c 100644 --- a/lib/livebook_web/live/session_live/secrets_component.ex +++ b/lib/livebook_web/live/session_live/secrets_component.ex @@ -109,6 +109,7 @@ defmodule LivebookWeb.SessionLive.SecretsComponent do /> <.radio_field field={f[:origin]} + value={SecretOrigin.encode(f[:origin].value)} label="Storage" options={ [{"session", "only this session"}, {"app", "in the Livebook app"}] ++ diff --git a/lib/livebook_web/live/session_live/secrets_list_component.ex b/lib/livebook_web/live/session_live/secrets_list_component.ex index 5a2ab8e22..276d6167c 100644 --- a/lib/livebook_web/live/session_live/secrets_list_component.ex +++ b/lib/livebook_web/live/session_live/secrets_list_component.ex @@ -111,6 +111,7 @@ defmodule LivebookWeb.SessionLive.SecretsListComponent do prefix={"hub-#{id}"} data_secrets={@secrets} hubs={@hubs} + myself={@myself} />
diff --git a/proto/lib/livebook_proto.ex b/proto/lib/livebook_proto.ex index 1e8039aa4..76ef26dcf 100644 --- a/proto/lib/livebook_proto.ex +++ b/proto/lib/livebook_proto.ex @@ -1,7 +1,14 @@ defmodule LivebookProto do @moduledoc false - alias LivebookProto.{Request, Response} + alias LivebookProto.{ + CreateSecretRequest, + CreateSecretResponse, + HandshakeRequest, + HandshakeResponse, + Request, + Response + } @request_mapping (for {_id, field_prop} <- Request.__message_props__().field_props, into: %{} do @@ -13,6 +20,13 @@ defmodule LivebookProto do {field_prop.type, field_prop.name_atom} end) + @type request_proto :: HandshakeRequest.t() | CreateSecretRequest.t() + @type response_proto :: HandshakeResponse.t() | CreateSecretResponse.t() + + @doc """ + Builds a request frame with given data and id. + """ + @spec build_request_frame(request_proto(), integer()) :: {:binary, iodata()} def build_request_frame(%struct{} = data, id \\ -1) do type = request_type(struct) message = Request.new!(id: id, type: {type, data}) @@ -20,6 +34,22 @@ defmodule LivebookProto do {:binary, Request.encode(message)} end + @doc """ + Builds a create secret request struct. + """ + @spec build_create_secret_request(keyword()) :: CreateSecretRequest.t() + defdelegate build_create_secret_request(fields), to: CreateSecretRequest, as: :new! + + @doc """ + Builds a handshake request struct. + """ + @spec build_handshake_request(keyword()) :: HandshakeRequest.t() + defdelegate build_handshake_request(fields), to: HandshakeRequest, as: :new! + + @doc """ + Builds a response with given data and id. + """ + @spec build_response(response_proto(), integer()) :: Response.t() def build_response(%struct{} = data, id \\ -1) do type = response_type(struct) Response.new!(id: id, type: {type, data}) diff --git a/proto/lib/livebook_proto/event.pb.ex b/proto/lib/livebook_proto/event.pb.ex index 5f71e048d..580e5ad1a 100644 --- a/proto/lib/livebook_proto/event.pb.ex +++ b/proto/lib/livebook_proto/event.pb.ex @@ -13,4 +13,14 @@ defmodule LivebookProto.Event do type: LivebookProto.SecretUpdated, json_name: "secretUpdated", oneof: 0 + + field :secret_deleted, 102, + type: LivebookProto.SecretDeleted, + json_name: "secretDeleted", + oneof: 0 + + field :user_synchronized, 103, + type: LivebookProto.UserSynchronized, + json_name: "userSynchronized", + oneof: 0 end diff --git a/proto/lib/livebook_proto/session_request.pb.ex b/proto/lib/livebook_proto/handshake_request.pb.ex similarity index 78% rename from proto/lib/livebook_proto/session_request.pb.ex rename to proto/lib/livebook_proto/handshake_request.pb.ex index bd5d0bc55..22d9eafc4 100644 --- a/proto/lib/livebook_proto/session_request.pb.ex +++ b/proto/lib/livebook_proto/handshake_request.pb.ex @@ -1,4 +1,4 @@ -defmodule LivebookProto.SessionRequest do +defmodule LivebookProto.HandshakeRequest do @moduledoc false use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 diff --git a/proto/lib/livebook_proto/session_response.pb.ex b/proto/lib/livebook_proto/handshake_response.pb.ex similarity index 50% rename from proto/lib/livebook_proto/session_response.pb.ex rename to proto/lib/livebook_proto/handshake_response.pb.ex index a87fd96e6..fc3d5b481 100644 --- a/proto/lib/livebook_proto/session_response.pb.ex +++ b/proto/lib/livebook_proto/handshake_response.pb.ex @@ -1,7 +1,8 @@ -defmodule LivebookProto.SessionResponse do +defmodule LivebookProto.HandshakeResponse do @moduledoc false use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 field :id, 1, type: :string - field :user, 2, type: LivebookProto.User + field :name, 2, type: :string + field :user, 3, type: LivebookProto.User end diff --git a/proto/lib/livebook_proto/request.pb.ex b/proto/lib/livebook_proto/request.pb.ex index 20dea2553..edcc3c6c6 100644 --- a/proto/lib/livebook_proto/request.pb.ex +++ b/proto/lib/livebook_proto/request.pb.ex @@ -5,7 +5,7 @@ defmodule LivebookProto.Request do oneof :type, 0 field :id, 1, type: :int32 - field :session, 2, type: LivebookProto.SessionRequest, oneof: 0 + field :handshake, 2, type: LivebookProto.HandshakeRequest, oneof: 0 field :create_secret, 3, type: LivebookProto.CreateSecretRequest, diff --git a/proto/lib/livebook_proto/response.pb.ex b/proto/lib/livebook_proto/response.pb.ex index 7be637b65..e158cd001 100644 --- a/proto/lib/livebook_proto/response.pb.ex +++ b/proto/lib/livebook_proto/response.pb.ex @@ -7,7 +7,7 @@ defmodule LivebookProto.Response do field :id, 1, type: :int32 field :error, 2, type: LivebookProto.Error, oneof: 0 field :changeset, 3, type: LivebookProto.ChangesetError, oneof: 0 - field :session, 4, type: LivebookProto.SessionResponse, oneof: 0 + field :handshake, 4, type: LivebookProto.HandshakeResponse, oneof: 0 field :create_secret, 5, type: LivebookProto.CreateSecretResponse, diff --git a/proto/lib/livebook_proto/secret.pb.ex b/proto/lib/livebook_proto/secret.pb.ex new file mode 100644 index 000000000..619f64061 --- /dev/null +++ b/proto/lib/livebook_proto/secret.pb.ex @@ -0,0 +1,7 @@ +defmodule LivebookProto.Secret do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 + + field :name, 1, type: :string + field :value, 2, type: :string +end diff --git a/proto/lib/livebook_proto/secret_deleted.pb.ex b/proto/lib/livebook_proto/secret_deleted.pb.ex new file mode 100644 index 000000000..c2ea54acd --- /dev/null +++ b/proto/lib/livebook_proto/secret_deleted.pb.ex @@ -0,0 +1,7 @@ +defmodule LivebookProto.SecretDeleted do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 + + field :name, 1, type: :string + field :value, 2, type: :string +end diff --git a/proto/lib/livebook_proto/user_synchronized.pb.ex b/proto/lib/livebook_proto/user_synchronized.pb.ex new file mode 100644 index 000000000..04a7073c5 --- /dev/null +++ b/proto/lib/livebook_proto/user_synchronized.pb.ex @@ -0,0 +1,8 @@ +defmodule LivebookProto.UserSynchronized do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 + + field :id, 1, type: :string + field :name, 2, type: :string + field :secrets, 3, repeated: true, type: LivebookProto.Secret +end diff --git a/proto/messages.proto b/proto/messages.proto index cb6b68306..1348710f2 100644 --- a/proto/messages.proto +++ b/proto/messages.proto @@ -5,6 +5,11 @@ message User { string email = 2; } +message Secret { + string name = 1; + string value = 2; +} + message Error { string details = 1; } @@ -28,13 +33,25 @@ message SecretUpdated { string value = 2; } -message SessionRequest { +message SecretDeleted { + string name = 1; + string value = 2; +} + +message UserSynchronized { + string id = 1; + string name = 2; + repeated Secret secrets = 3; +} + +message HandshakeRequest { string app_version = 1; } -message SessionResponse { +message HandshakeResponse { string id = 1; - User user = 2; + string name = 2; + User user = 3; } message CreateSecretRequest { @@ -49,7 +66,7 @@ message Request { int32 id = 1; oneof type { - SessionRequest session = 2; + HandshakeRequest handshake = 2; CreateSecretRequest create_secret = 3; } } @@ -61,7 +78,7 @@ message Response { Error error = 2; ChangesetError changeset = 3; - SessionResponse session = 4; + HandshakeResponse handshake = 4; CreateSecretResponse create_secret = 5; } } @@ -70,5 +87,7 @@ message Event { oneof type { SecretCreated secret_created = 100; SecretUpdated secret_updated = 101; + SecretDeleted secret_deleted = 102; + UserSynchronized user_synchronized = 103; } } diff --git a/test/livebook/hubs/enterprise_client_test.exs b/test/livebook/hubs/enterprise_client_test.exs index 788b95c28..b7c2d0a14 100644 --- a/test/livebook/hubs/enterprise_client_test.exs +++ b/test/livebook/hubs/enterprise_client_test.exs @@ -55,22 +55,45 @@ defmodule Livebook.Hubs.EnterpriseClientTest do name = "API_TOKEN_ID" value = Livebook.Utils.random_id() :erpc.call(node, Enterprise.Integration, :create_secret, [name, value]) + secret = %Secret{name: name, value: value, origin: {:hub, id}} - assert_receive {:secret_created, %Secret{name: ^name, value: ^value, origin: {:hub, ^id}}} + assert_receive {:secret_created, ^secret} + assert secret in EnterpriseClient.get_secrets(id) end test "receives a secret_updated event", %{node: node, hub_id: id} do name = "SUPER_SUDO_USER" value = "JakePeralta" - secret = :erpc.call(node, Enterprise.Integration, :create_secret, [name, 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]) + enterprise_secret = :erpc.call(node, Enterprise.Integration, :create_secret, [name, value]) + secret = %Secret{name: name, value: value, origin: {:hub, id}} + updated_secret = %Secret{name: name, value: new_value, origin: {:hub, id}} - assert_receive {:secret_updated, - %Secret{name: ^name, value: ^new_value, origin: {:hub, ^id}}} + assert_receive {:secret_created, ^secret} + assert secret in EnterpriseClient.get_secrets(id) + refute updated_secret in EnterpriseClient.get_secrets(id) + + :erpc.call(node, Enterprise.Integration, :update_secret, [enterprise_secret, new_value]) + + assert_receive {:secret_updated, ^updated_secret} + + assert updated_secret in EnterpriseClient.get_secrets(id) + refute secret in EnterpriseClient.get_secrets(id) + end + + test "receives a secret_deleted event", %{node: node, hub_id: id} do + name = "SUPER_DELETE" + value = "JakePeralta" + enteprise_secret = :erpc.call(node, Enterprise.Integration, :create_secret, [name, value]) + secret = %Secret{name: name, value: value, origin: {:hub, id}} + + assert_receive {:secret_created, ^secret} + assert secret in EnterpriseClient.get_secrets(id) + + :erpc.call(node, Enterprise.Integration, :delete_secret, [enteprise_secret]) + + assert_receive {:secret_deleted, ^secret} + refute secret in EnterpriseClient.get_secrets(id) end end end diff --git a/test/livebook/hubs/provider_test.exs b/test/livebook/hubs/provider_test.exs index 57e278342..9e4f263c7 100644 --- a/test/livebook/hubs/provider_test.exs +++ b/test/livebook/hubs/provider_test.exs @@ -1,30 +1,77 @@ defmodule Livebook.Hubs.ProviderTest do use Livebook.DataCase - alias Livebook.Hubs.{Fly, Metadata, Provider} + alias Livebook.Hubs.Provider + alias Livebook.Secrets - describe "Fly" do - test "to_metadata/1" do - fly = build(:fly) - - assert Provider.to_metadata(fly) == %Metadata{ - id: fly.id, - name: fly.hub_name, - emoji: fly.hub_emoji, - provider: fly, - connected?: false - } + describe "personal" do + setup do + {:ok, hub: build(:personal)} end - test "load/2" do - fly = build(:fly) - fields = Map.from_struct(fly) - - assert Provider.load(%Fly{}, fields) == fly + test "load/2", %{hub: hub} do + assert Provider.load(hub, Map.from_struct(hub)) == hub end - test "type/1" do - assert Provider.type(%Fly{}) == "fly" + test "type/1", %{hub: hub} do + assert Provider.type(hub) == "personal" + end + + test "connection_spec/1", %{hub: hub} do + refute Provider.connection_spec(hub) + end + + test "disconnect/1", %{hub: hub} do + assert_raise RuntimeError, "not implemented", fn -> Provider.disconnect(hub) end + end + + test "capabilities/1", %{hub: hub} do + assert Provider.capabilities(hub) == [ + :list_secrets, + :create_secret, + :update_secret, + :delete_secret + ] + end + + test "get_secrets/1 without startup secrets", %{hub: hub} do + secret = insert_secret(name: "GET_PERSONAL_SECRET", origin: {:hub, "personal-hub"}) + assert secret in Provider.get_secrets(hub) + end + + test "get_secrets/1 with startup secrets", %{hub: hub} do + secret = build(:secret, name: "GET_PERSONAL_SECRET", origin: :startup) + Livebook.Secrets.set_startup_secrets([secret]) + + assert secret in Provider.get_secrets(hub) + end + + test "create_secret/1", %{hub: hub} do + secret = build(:secret, name: "CREATE_PERSONAL_SECRET", origin: {:hub, "personal-hub"}) + assert Provider.create_secret(hub, secret) == :ok + end + + test "update_secret/1", %{hub: hub} do + secret = insert_secret(name: "UPDATE_PERSONAL_SECRET", origin: {:hub, "personal-hub"}) + assert secret in Secrets.get_secrets() + + updated_secret = %{secret | value: "123321"} + + assert Provider.update_secret(hub, updated_secret) == :ok + assert updated_secret in Secrets.get_secrets() + refute secret in Secrets.get_secrets() + end + + test "delete_secret/1", %{hub: hub} do + secret = insert_secret(name: "DELETE_PERSONAL_SECRET", origin: {:hub, "personal-hub"}) + assert secret in Secrets.get_secrets() + + assert Provider.delete_secret(hub, secret) == :ok + refute secret in Secrets.get_secrets() + end + + test "connection_error/1", %{hub: hub} do + assert_raise RuntimeError, "not implemented", fn -> Provider.connection_error(hub) end end end end diff --git a/test/livebook/secrets_test.exs b/test/livebook/secrets_test.exs index fd1fcb4a9..ed42881f3 100644 --- a/test/livebook/secrets_test.exs +++ b/test/livebook/secrets_test.exs @@ -19,7 +19,7 @@ defmodule Livebook.SecretsTest do test "returns a list of secrets from temporary storage" do secret = build(:secret, name: "FOO", value: "222", origin: :startup) - Secrets.set_temporary_secrets([secret]) + Secrets.set_startup_secrets([secret]) assert secret in Secrets.get_secrets() # We can't delete from temporary storage, since it will be deleted diff --git a/test/livebook/web_socket/client_connection_test.exs b/test/livebook/web_socket/client_connection_test.exs index 85a52489d..c391a9b14 100644 --- a/test/livebook/web_socket/client_connection_test.exs +++ b/test/livebook/web_socket/client_connection_test.exs @@ -46,33 +46,20 @@ defmodule Livebook.WebSocket.ClientConnectionTest do end test "successfully sends a session request", %{conn: conn, user: %{id: id, email: email}} do - session_request = - LivebookProto.SessionRequest.new!(app_version: Livebook.Config.app_version()) + data = LivebookProto.build_handshake_request(app_version: Livebook.Config.app_version()) - assert {:session, session_response} = ClientConnection.send_request(conn, session_request) - assert %{id: _, user: %{id: ^id, email: ^email}} = session_response + assert {:handshake, handshake_response} = ClientConnection.send_request(conn, data) + assert %{id: _, user: %{id: ^id, email: ^email}} = handshake_response end test "successfully sends a create secret message", %{conn: conn} do - create_secret_request = - LivebookProto.CreateSecretRequest.new!( - name: "MY_USERNAME", - value: "Jake Peralta" - ) - - assert {:create_secret, _} = ClientConnection.send_request(conn, create_secret_request) + data = LivebookProto.build_create_secret_request(name: "MY_USERNAME", value: "Jake Peralta") + assert {:create_secret, _} = ClientConnection.send_request(conn, data) end test "sends a create secret message, but receive a changeset error", %{conn: conn} do - create_secret_request = - LivebookProto.CreateSecretRequest.new!( - name: "MY_USERNAME", - value: "" - ) - - assert {:changeset_error, errors} = - ClientConnection.send_request(conn, create_secret_request) - + data = LivebookProto.build_create_secret_request(name: "MY_USERNAME", value: "") + assert {:changeset_error, errors} = ClientConnection.send_request(conn, data) assert "can't be blank" in errors.value end end @@ -121,25 +108,23 @@ defmodule Livebook.WebSocket.ClientConnectionTest do setup %{url: url, token: token} do headers = [{"X-Auth-Token", token}] - {:ok, _conn} = ClientConnection.start_link(self(), url, headers) + {:ok, conn} = ClientConnection.start_link(self(), url, headers) assert_receive {:connect, :ok, :connected} - :ok + {:ok, conn: conn} end - test "receives a secret_created event" do + test "receives a secret_created event", %{node: node} do name = "MY_SECRET_ID" value = Livebook.Utils.random_id() - node = EnterpriseServer.get_node() :erpc.call(node, Enterprise.Integration, :create_secret, [name, value]) assert_receive {:event, :secret_created, %{name: ^name, value: ^value}} end - test "receives a secret_updated event" do + test "receives a secret_updated event", %{node: node} do name = "API_USERNAME" value = "JakePeralta" - node = EnterpriseServer.get_node() secret = :erpc.call(node, Enterprise.Integration, :create_secret, [name, value]) assert_receive {:event, :secret_created, %{name: ^name, value: ^value}} @@ -149,5 +134,35 @@ defmodule Livebook.WebSocket.ClientConnectionTest do assert_receive {:event, :secret_updated, %{name: ^name, value: ^new_value}} end + + test "receives a secret_deleted event", %{node: node} do + name = "DELETE_ME" + value = "JakePeralta" + secret = :erpc.call(node, Enterprise.Integration, :create_secret, [name, value]) + + assert_receive {:event, :secret_created, %{name: ^name, value: ^value}} + + :erpc.call(node, Enterprise.Integration, :delete_secret, [secret]) + + assert_receive {:event, :secret_deleted, %{name: ^name, value: ^value}} + end + + test "receives a session_created event", %{conn: conn, node: node} do + data = LivebookProto.build_handshake_request(app_version: Livebook.Config.app_version()) + assert {:handshake, _} = ClientConnection.send_request(conn, data) + + id = :erpc.call(node, Enterprise.Integration, :fetch_env!, ["ENTERPRISE_ID"]) + name = :erpc.call(node, Enterprise.Integration, :fetch_env!, ["ENTERPRISE_NAME"]) + + assert_receive {:event, :user_synchronized, %{id: ^id, name: ^name, secrets: []}} + + secret = :erpc.call(node, Enterprise.Integration, :create_secret, ["SESSION", "123"]) + assert_receive {:event, :secret_created, %{name: "SESSION", value: "123"}} + + assert {:handshake, _} = ClientConnection.send_request(conn, data) + + assert_receive {:event, :user_synchronized, %{id: ^id, name: ^name, secrets: secrets}} + assert LivebookProto.Secret.new!(name: secret.name, value: secret.value) in secrets + end end end diff --git a/test/livebook_web/live/session_live/secrets_component_test.exs b/test/livebook_web/live/session_live/secrets_component_test.exs index 594806b0b..a007f0d92 100644 --- a/test/livebook_web/live/session_live/secrets_component_test.exs +++ b/test/livebook_web/live/session_live/secrets_component_test.exs @@ -60,13 +60,8 @@ defmodule LivebookWeb.SessionLive.SecretsComponentTest do assert_receive {:secret_created, ^secret} assert render(view) =~ "A new secret has been created on your Livebook Enterprise" - assert has_element?(view, "#hub-#{enterprise.id}-secret-#{secret.name}-title") - - assert has_element?( - view, - "#hub-#{enterprise.id}-secret-#{secret.name}-title span", - enterprise.hub_emoji - ) + assert has_element?(view, "#hub-#{enterprise.id}-secret-#{secret.name}-wrapper") + assert has_element?(view, ~s/[data-tooltip="#{enterprise.hub_name}"]/, enterprise.hub_emoji) end test "toggle a secret from Enterprise hub", diff --git a/test/livebook_web/live/session_live_test.exs b/test/livebook_web/live/session_live_test.exs index 2135008ab..2dd18a73a 100644 --- a/test/livebook_web/live/session_live_test.exs +++ b/test/livebook_web/live/session_live_test.exs @@ -1217,7 +1217,7 @@ defmodule LivebookWeb.SessionLiveTest do test "loads secret from temporary storage", %{conn: conn, session: session} do secret = build(:secret, name: "FOOBARBAZ", value: "ChonkyCat", origin: :startup) - Livebook.Secrets.set_temporary_secrets([secret]) + Livebook.Secrets.set_startup_secrets([secret]) {:ok, view, _} = live(conn, ~p"/sessions/#{session.id}") @@ -1246,7 +1246,7 @@ defmodule LivebookWeb.SessionLiveTest do test "granting access for unavailable startup secret using 'Add secret' button", %{conn: conn, session: session} do secret = build(:secret, name: "MYSTARTUPSECRET", value: "ChonkyCat", origin: :startup) - Livebook.Secrets.set_temporary_secrets([secret]) + Livebook.Secrets.set_startup_secrets([secret]) # Subscribe and executes the code to trigger # the `System.EnvError` exception and outputs the 'Add secret' button diff --git a/test/support/factory.ex b/test/support/factory.ex index 6e0fede69..79b972b3d 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -43,6 +43,18 @@ defmodule Livebook.Factory do } end + def build(:personal_metadata) do + :personal |> build() |> Livebook.Hubs.Provider.to_metadata() + end + + def build(:personal) do + %Livebook.Hubs.Personal{ + id: "personal-hub", + hub_name: "My Hub", + hub_emoji: "🏠" + } + end + def build(:env_var) do %Livebook.Settings.EnvVar{ name: "BAR", @@ -58,11 +70,11 @@ defmodule Livebook.Factory do } end - def build(factory_name, attrs \\ %{}) do + def build(factory_name, attrs) do factory_name |> build() |> struct!(attrs) end - def params_for(factory_name, attrs \\ %{}) do + def params_for(factory_name, attrs) do factory_name |> build() |> struct!(attrs) |> Map.from_struct() end diff --git a/test/support/integration/enterprise_server.ex b/test/support/integration/enterprise_server.ex index 2cff0a5c9..b0a1d72e7 100644 --- a/test/support/integration/enterprise_server.ex +++ b/test/support/integration/enterprise_server.ex @@ -220,6 +220,10 @@ defmodule Livebook.EnterpriseServer do System.get_env("ENTERPRISE_DEBUG", "false") end + defp proto do + System.get_env("ENTERPRISE_LIVEBOOK_PROTO_PATH") + end + defp wait_on_start(state, port) do url = state.url || fetch_url(state) @@ -265,6 +269,8 @@ defmodule Livebook.EnterpriseServer do "DEBUG" => debug() } + env = if proto(), do: Map.merge(env, %{"LIVEBOOK_PROTO_PATH" => proto()}), else: env + if state_env do Map.merge(env, state_env) else