diff --git a/README.md b/README.md index b121edcc7..276bf1295 100644 --- a/README.md +++ b/README.md @@ -261,6 +261,10 @@ The following environment variables can be used to configure Livebook on boot: * LIVEBOOK_TEAMS_OFFLINE_KEY - sets the Livebook Teams public key for creating an offline hub. Must be set together with LIVEBOOK_TEAMS_NAME and LIVEBOOK_TEAMS_KEY. + * LIVEBOOK_TEAMS_SECRETS - sets the Livebook Teams encrypted secrets for deploying apps with secrets. + This is relevant when deploying apps with offline hub. Must be set together with + LIVEBOOK_TEAMS_NAME, LIVEBOOK_TEAMS_KEY and LIVEBOOK_TEAMS_OFFLINE_KEY. + * LIVEBOOK_TOKEN_ENABLED - controls whether token authentication is enabled. Enabled by default unless LIVEBOOK_PASSWORD is set. Set it to "false" to disable it. diff --git a/lib/livebook/application.ex b/lib/livebook/application.ex index c55f45180..25fcad327 100644 --- a/lib/livebook/application.ex +++ b/lib/livebook/application.ex @@ -217,6 +217,7 @@ defmodule Livebook.Application do def create_offline_hub() do name = System.get_env("LIVEBOOK_TEAMS_NAME") public_key = System.get_env("LIVEBOOK_TEAMS_OFFLINE_KEY") + encrypted_secrets = System.get_env("LIVEBOOK_TEAMS_SECRETS") if name && public_key do teams_key = @@ -225,12 +226,43 @@ defmodule Livebook.Application do "You specified LIVEBOOK_TEAMS_NAME, but LIVEBOOK_TEAMS_KEY is missing." ) - Livebook.Hubs.set_offline_hub(%Livebook.Hubs.Team{ + id = "team-#{name}" + + secrets = + if encrypted_secrets do + {secret_key, sign_secret} = Livebook.Teams.derive_keys(teams_key) + + case Livebook.Teams.decrypt(encrypted_secrets, secret_key, sign_secret) do + {:ok, json} -> + for {name, value} <- Jason.decode!(json), + do: %Livebook.Secrets.Secret{ + name: name, + value: value, + hub_id: id + } + + :error -> + Livebook.Config.abort!( + "You specified LIVEBOOK_TEAMS_SECRETS, but we couldn't decrypt with the given LIVEBOOK_TEAMS_KEY." + ) + end + else + [] + end + + Livebook.Hubs.save_hub(%Livebook.Hubs.Team{ id: "team-#{name}", hub_name: name, hub_emoji: "💡", + user_id: 0, + org_id: 0, + org_key_id: 0, + session_token: "", teams_key: teams_key, - org_public_key: public_key + org_public_key: public_key, + offline: %Livebook.Hubs.Team.Offline{ + secrets: secrets + } }) end end diff --git a/lib/livebook/apps.ex b/lib/livebook/apps.ex index cfa7f0524..d600a9a0e 100644 --- a/lib/livebook/apps.ex +++ b/lib/livebook/apps.ex @@ -234,8 +234,7 @@ defmodule Livebook.Apps do for path <- Path.wildcard(pattern) do markdown = File.read!(path) - {notebook, %{warnings: warnings, verified_hub_id: verified_hub_id}} = - Livebook.LiveMarkdown.notebook_from_livemd(markdown) + {notebook, warnings} = Livebook.LiveMarkdown.notebook_from_livemd(markdown) apps_path_hub_id = Livebook.Config.apps_path_hub_id() @@ -245,7 +244,7 @@ defmodule Livebook.Apps do {:error, "the deployment settings are missing or invalid. Please configure them under the notebook deploy panel"} - apps_path_hub_id && apps_path_hub_id != verified_hub_id -> + apps_path_hub_id && apps_path_hub_id != notebook.hub_id -> {:error, "the notebook is not verified to come from hub #{apps_path_hub_id}"} true -> diff --git a/lib/livebook/hubs.ex b/lib/livebook/hubs.ex index fd4cc19c1..bd51460bd 100644 --- a/lib/livebook/hubs.ex +++ b/lib/livebook/hubs.ex @@ -6,6 +6,7 @@ defmodule Livebook.Hubs do alias Livebook.Secrets.Secret @namespace :hubs + @supervisor Livebook.HubsSupervisor @type connected_hub :: %{ required(:pid) => pid(), @@ -79,8 +80,8 @@ defmodule Livebook.Hubs do """ @spec save_hub(Provider.t()) :: Provider.t() def save_hub(struct) do - attributes = struct |> Map.from_struct() |> Map.to_list() - :ok = Storage.insert(@namespace, struct.id, attributes) + attributes = Provider.dump(struct) + :ok = Storage.insert(@namespace, struct.id, Map.to_list(attributes)) :ok = connect_hub(struct) :ok = Broadcasts.hub_changed(struct.id) @@ -164,7 +165,7 @@ defmodule Livebook.Hubs do end defp to_struct(%{id: "team-" <> _} = fields) do - Provider.load(%Team{}, fields) + Provider.load(Team.new(), fields) end @doc """ @@ -185,7 +186,7 @@ defmodule Livebook.Hubs do defp connect_hub(hub) do if child_spec = Provider.connection_spec(hub) do - DynamicSupervisor.start_child(Livebook.HubsSupervisor, child_spec) + DynamicSupervisor.start_child(@supervisor, child_spec) end :ok @@ -272,25 +273,4 @@ defmodule Livebook.Hubs do def capability?(hub, capabilities) do capabilities -- Provider.capabilities(hub) == [] end - - @offline_hub_key :livebook_offline_hub - - @doc """ - Gets the offline hub if the given id matches. - """ - @spec get_offline_hub(String.t()) :: Provider.t() | nil - def get_offline_hub(id) do - case :persistent_term.get(@offline_hub_key, nil) do - %{id: ^id} = hub -> hub - _ -> nil - end - end - - @doc """ - Sets a offline hub that will be kept only in memory. - """ - @spec set_offline_hub(Provider.t()) :: :ok - def set_offline_hub(hub) do - :persistent_term.put(@offline_hub_key, hub) - end end diff --git a/lib/livebook/hubs/personal.ex b/lib/livebook/hubs/personal.ex index fb91768d6..5bd83f659 100644 --- a/lib/livebook/hubs/personal.ex +++ b/lib/livebook/hubs/personal.ex @@ -142,17 +142,21 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Personal do :skip end - def notebook_stamp(hub, notebook_source, metadata) do - token = Livebook.Stamping.aead_encrypt(metadata, notebook_source, hub.secret_key) + def notebook_stamp(personal, notebook_source, metadata) do + token = Livebook.Stamping.aead_encrypt(metadata, notebook_source, personal.secret_key) stamp = %{"version" => 1, "token" => token} {:ok, stamp} end - def verify_notebook_stamp(hub, notebook_source, stamp) do + def verify_notebook_stamp(personal, notebook_source, stamp) do %{"version" => 1, "token" => token} = stamp - Livebook.Stamping.aead_decrypt(token, notebook_source, hub.secret_key) + Livebook.Stamping.aead_decrypt(token, notebook_source, personal.secret_key) + end + + def dump(personal) do + Map.from_struct(personal) end end diff --git a/lib/livebook/hubs/provider.ex b/lib/livebook/hubs/provider.ex index c5f1b58ee..8d986884f 100644 --- a/lib/livebook/hubs/provider.ex +++ b/lib/livebook/hubs/provider.ex @@ -112,4 +112,10 @@ defprotocol Livebook.Hubs.Provider do @spec verify_notebook_stamp(t(), iodata(), notebook_stamp()) :: {:ok, metadata :: map()} | :error def verify_notebook_stamp(hub, notebook_source, stamp) + + @doc """ + Transforms hub to the attributes map sent to storage. + """ + @spec dump(t()) :: map() + def dump(hub) end diff --git a/lib/livebook/hubs/team.ex b/lib/livebook/hubs/team.ex index 8fb10feb8..572f293fd 100644 --- a/lib/livebook/hubs/team.ex +++ b/lib/livebook/hubs/team.ex @@ -4,18 +4,37 @@ defmodule Livebook.Hubs.Team do use Ecto.Schema import Ecto.Changeset + defmodule Offline do + @moduledoc false + use Ecto.Schema + + alias Livebook.Secrets.Secret + + @type t :: %__MODULE__{ + secrets: list(Secret.t()) + } + + @primary_key false + embedded_schema do + field :secrets, {:array, :map}, default: [] + end + end + @type t :: %__MODULE__{ id: String.t() | nil, - org_id: pos_integer() | nil, - user_id: pos_integer() | nil, - org_key_id: pos_integer() | nil, - teams_key: String.t() | nil, - org_public_key: String.t() | nil, - session_token: String.t() | nil, + org_id: non_neg_integer(), + user_id: non_neg_integer(), + org_key_id: non_neg_integer(), + teams_key: String.t(), + org_public_key: String.t(), + session_token: String.t(), hub_name: String.t() | nil, - hub_emoji: String.t() | nil + hub_emoji: String.t() | nil, + offline: Offline.t() | nil } + @enforce_keys [:user_id, :org_id, :org_key_id, :session_token, :org_public_key, :teams_key] + embedded_schema do field :org_id, :integer field :user_id, :integer @@ -25,6 +44,8 @@ defmodule Livebook.Hubs.Team do field :session_token, :string field :hub_name, :string field :hub_emoji, :string + + embeds_one :offline, Offline end @fields ~w( @@ -38,6 +59,20 @@ defmodule Livebook.Hubs.Team do hub_emoji )a + @doc """ + Initializes a new Team hub. + """ + def new() do + %__MODULE__{ + user_id: nil, + org_id: nil, + org_key_id: nil, + session_token: nil, + org_public_key: nil, + teams_key: nil + } + end + @doc """ Returns an `%Ecto.Changeset{}` for tracking hub changes. """ @@ -101,7 +136,7 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Team do "Cannot connect to Hub: #{reason}. Will attempt to reconnect automatically..." end - def notebook_stamp(hub, notebook_source, metadata) do + def notebook_stamp(team, notebook_source, metadata) do # We apply authenticated encryption using the shared teams key, # just as for the personal hub, but we additionally sign the token # with a private organization key stored on the Teams server. We @@ -111,9 +146,9 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Team do # stamp requires access to the shared local key and an authenticated # request to the Teams server (which ensures team membership). - token = Livebook.Stamping.aead_encrypt(metadata, notebook_source, hub.teams_key) + token = Livebook.Stamping.aead_encrypt(metadata, notebook_source, team.teams_key) - case Livebook.Teams.org_sign(hub, token) do + case Livebook.Teams.org_sign(team, token) do {:ok, token_signature} -> stamp = %{"version" => 1, "token" => token, "token_signature" => token_signature} {:ok, stamp} @@ -123,13 +158,19 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Team do end end - def verify_notebook_stamp(hub, notebook_source, stamp) do + def verify_notebook_stamp(team, notebook_source, stamp) do %{"version" => 1, "token" => token, "token_signature" => token_signature} = stamp - if Livebook.Stamping.rsa_verify?(token_signature, token, hub.org_public_key) do - Livebook.Stamping.aead_decrypt(token, notebook_source, hub.teams_key) + if Livebook.Stamping.rsa_verify?(token_signature, token, team.org_public_key) do + Livebook.Stamping.aead_decrypt(token, notebook_source, team.teams_key) else :error end end + + def dump(team) do + team + |> Map.from_struct() + |> Map.delete(:offline) + end end diff --git a/lib/livebook/hubs/team_client.ex b/lib/livebook/hubs/team_client.ex index 56abdb6f6..ab41f0622 100644 --- a/lib/livebook/hubs/team_client.ex +++ b/lib/livebook/hubs/team_client.ex @@ -66,7 +66,9 @@ defmodule Livebook.Hubs.TeamClient do ## GenServer callbacks @impl true - def init(%Team{} = team) do + def init(%Team{offline: nil} = team) do + derived_keys = Teams.derive_keys(team.teams_key) + headers = [ {"x-user", to_string(team.user_id)}, {"x-org", to_string(team.org_id)}, @@ -74,12 +76,16 @@ defmodule Livebook.Hubs.TeamClient do {"x-session-token", team.session_token} ] - derived_keys = Teams.derive_keys(team.teams_key) - {:ok, _pid} = Connection.start_link(self(), headers) {:ok, %__MODULE__{hub: team, derived_keys: derived_keys}} end + def init(%Team{} = team) do + derived_keys = Teams.derive_keys(team.teams_key) + + {:ok, %__MODULE__{hub: team, secrets: team.offline.secrets, derived_keys: derived_keys}} + end + @impl true def handle_call(:get_connection_error, _caller, state) do {:reply, state.connection_error, state} @@ -134,7 +140,7 @@ defmodule Livebook.Hubs.TeamClient do defp build_secret(state, %{name: name, value: value}) do {secret_key, sign_secret} = state.derived_keys - {:ok, decrypted_value} = Teams.decrypt_secret_value(value, secret_key, sign_secret) + {:ok, decrypted_value} = Teams.decrypt(value, secret_key, sign_secret) %Livebook.Secrets.Secret{ name: name, @@ -164,15 +170,7 @@ defmodule Livebook.Hubs.TeamClient do remove_secret(state, secret) end - defp handle_event(:user_connected, user_connected, %{secrets: []} = state) do - secrets = for secret <- user_connected.secrets, do: build_secret(state, secret) - - %{state | secrets: secrets} - end - - defp handle_event(:user_connected, user_connected, state) do - secrets = for secret <- user_connected.secrets, do: build_secret(state, secret) - + defp handle_event(:user_connected, %{secrets: secrets}, state) do created_secrets = Enum.reject(secrets, fn secret -> Enum.find(state.secrets, &(&1.name == secret.name and &1.value == secret.value)) @@ -188,15 +186,15 @@ defmodule Livebook.Hubs.TeamClient do Enum.find(state.secrets, &(&1.name == secret.name and &1.value != secret.value)) end) - events_by_topic = [ + secrets_by_topic = [ secret_deleted: deleted_secrets, secret_created: created_secrets, secret_updated: updated_secrets ] - for {topic, events} <- events_by_topic, - event <- events, + for {topic, secrets} <- secrets_by_topic, + secret <- secrets, reduce: state, - do: (acc -> handle_event(topic, event, acc)) + do: (acc -> handle_event(topic, secret, acc)) end end diff --git a/lib/livebook/live_markdown.ex b/lib/livebook/live_markdown.ex index 3c2431750..64353b434 100644 --- a/lib/livebook/live_markdown.ex +++ b/lib/livebook/live_markdown.ex @@ -98,21 +98,9 @@ defmodule Livebook.LiveMarkdown do @doc """ Converts the given Markdown document into a notebook data structure. - Returns the notebook struct and info map with the following keys: - - * `:warnings` - a list of informative messages/warnings related - to the imported input - - * `:verified_hub_id` - the hub id specified in the notebook - provided that the stamp was successfully verified, `nil` - otherwise - + Returns the notebook structure and a list of informative messages/warnings + related to the imported input. """ - @spec notebook_from_livemd(String.t()) :: - {Notebook.t(), - %{ - required(:warnings) => list(String.t()), - required(:verified_hub_id) => String.t() | nil - }} + @spec notebook_from_livemd(String.t()) :: {Notebook.t(), list(String.t())} defdelegate notebook_from_livemd(markdown), to: Livebook.LiveMarkdown.Import end diff --git a/lib/livebook/live_markdown/import.ex b/lib/livebook/live_markdown/import.ex index d8399cd04..2bacf0d47 100644 --- a/lib/livebook/live_markdown/import.ex +++ b/lib/livebook/live_markdown/import.ex @@ -10,17 +10,17 @@ defmodule Livebook.LiveMarkdown.Import do {ast, rewrite_messages} = rewrite_ast(ast) elements = group_elements(ast) {stamp_data, elements} = take_stamp_data(elements) - {notebook, stamp_hub_id, build_messages} = build_notebook(elements) + {notebook, build_messages} = build_notebook(elements) {notebook, postprocess_messages} = postprocess_notebook(notebook) - {notebook, verified_hub_id, metadata_messages} = - postprocess_stamp(notebook, markdown, stamp_data, stamp_hub_id) + {notebook, metadata_messages} = + postprocess_stamp(notebook, markdown, stamp_data) messages = earmark_messages ++ rewrite_messages ++ build_messages ++ postprocess_messages ++ metadata_messages - {notebook, %{warnings: messages, verified_hub_id: verified_hub_id}} + {notebook, messages} end defp earmark_message_to_string({_severity, line_number, message}) do @@ -329,7 +329,7 @@ defmodule Livebook.LiveMarkdown.Import do ] end - {attrs, stamp_hub_id, metadata_messages} = notebook_metadata_to_attrs(metadata) + {attrs, metadata_messages} = notebook_metadata_to_attrs(metadata) messages = messages ++ metadata_messages # We identify a single leading cell as the setup cell, in any @@ -352,7 +352,7 @@ defmodule Livebook.LiveMarkdown.Import do |> maybe_put_setup_cell(setup_cell) |> Map.merge(attrs) - {notebook, stamp_hub_id, messages} + {notebook, messages} end defp maybe_put_name(notebook, nil), do: notebook @@ -380,35 +380,34 @@ defmodule Livebook.LiveMarkdown.Import do defp grab_leading_comments(elems), do: {[], elems} defp notebook_metadata_to_attrs(metadata) do - Enum.reduce(metadata, {%{}, Livebook.Hubs.Personal.id(), []}, fn - {"persist_outputs", persist_outputs}, {attrs, stamp_hub_id, messages} -> - {Map.put(attrs, :persist_outputs, persist_outputs), stamp_hub_id, messages} + Enum.reduce(metadata, {%{}, []}, fn + {"persist_outputs", persist_outputs}, {attrs, messages} -> + {Map.put(attrs, :persist_outputs, persist_outputs), messages} - {"autosave_interval_s", autosave_interval_s}, {attrs, stamp_hub_id, messages} -> - {Map.put(attrs, :autosave_interval_s, autosave_interval_s), stamp_hub_id, messages} + {"autosave_interval_s", autosave_interval_s}, {attrs, messages} -> + {Map.put(attrs, :autosave_interval_s, autosave_interval_s), messages} - {"default_language", default_language}, {attrs, stamp_hub_id, messages} + {"default_language", default_language}, {attrs, messages} when default_language in ["elixir", "erlang"] -> default_language = String.to_atom(default_language) - {Map.put(attrs, :default_language, default_language), stamp_hub_id, messages} + {Map.put(attrs, :default_language, default_language), messages} - {"hub_id", hub_id}, {attrs, stamp_hub_id, messages} -> + {"hub_id", hub_id}, {attrs, messages} -> cond do - Hubs.hub_exists?(hub_id) -> {Map.put(attrs, :hub_id, hub_id), hub_id, messages} - Hubs.get_offline_hub(hub_id) -> {attrs, hub_id, messages} - true -> {attrs, stamp_hub_id, messages ++ ["ignoring notebook Hub with unknown id"]} + Hubs.hub_exists?(hub_id) -> {Map.put(attrs, :hub_id, hub_id), messages} + true -> {attrs, messages ++ ["ignoring notebook Hub with unknown id"]} end - {"app_settings", app_settings_metadata}, {attrs, stamp_hub_id, messages} -> + {"app_settings", app_settings_metadata}, {attrs, messages} -> app_settings = Map.merge( Notebook.AppSettings.new(), app_settings_metadata_to_attrs(app_settings_metadata) ) - {Map.put(attrs, :app_settings, app_settings), stamp_hub_id, messages} + {Map.put(attrs, :app_settings, app_settings), messages} - {"file_entries", file_entry_metadatas}, {attrs, stamp_hub_id, messages} + {"file_entries", file_entry_metadatas}, {attrs, messages} when is_list(file_entry_metadatas) -> file_system_by_id = if Enum.any?(file_entry_metadatas, &(&1["type"] == "file")) do @@ -438,10 +437,10 @@ defmodule Livebook.LiveMarkdown.Import do |> Map.put(:file_entries, file_entries) |> Map.put(:quarantine_file_entry_names, quarantine_file_entry_names) - {attrs, stamp_hub_id, messages ++ file_entry_messages} + {attrs, messages ++ file_entry_messages} - _entry, {attrs, stamp_hub_id, messages} -> - {attrs, stamp_hub_id, messages} + _entry, {attrs, messages} -> + {attrs, messages} end) end @@ -603,14 +602,10 @@ defmodule Livebook.LiveMarkdown.Import do defp take_stamp_data([{:stamp, data} | elements]), do: {data, elements} defp take_stamp_data(elements), do: {nil, elements} - defp postprocess_stamp(notebook, _notebook_source, nil, _), do: {notebook, nil, []} + defp postprocess_stamp(notebook, _notebook_source, nil), do: {notebook, []} - defp postprocess_stamp(notebook, notebook_source, stamp_data, stamp_hub_id) do - {hub, offline?} = - cond do - hub = Hubs.get_offline_hub(stamp_hub_id) -> {hub, true} - hub = Hubs.fetch_hub!(stamp_hub_id) -> {hub, false} - end + defp postprocess_stamp(notebook, notebook_source, stamp_data) do + hub = Hubs.fetch_hub!(notebook.hub_id) {valid_stamp?, notebook, messages} = with %{"offset" => offset, "stamp" => stamp} <- stamp_data, @@ -624,12 +619,9 @@ defmodule Livebook.LiveMarkdown.Import do # We enable teams features for offline hub only if the stamp # is valid, which ensures it is an existing Teams hub - teams_enabled = is_struct(hub, Livebook.Hubs.Team) and (not offline? or valid_stamp?) + teams_enabled = is_struct(hub, Livebook.Hubs.Team) and (hub.offline == nil or valid_stamp?) - notebook = %{notebook | teams_enabled: teams_enabled} - verified_hub_id = if(valid_stamp?, do: hub.id) - - {notebook, verified_hub_id, messages} + {%{notebook | teams_enabled: teams_enabled}, messages} end defp safe_binary_slice(binary, start, size) diff --git a/lib/livebook/notebook/learn.ex b/lib/livebook/notebook/learn.ex index 4840b32ff..1182c615d 100644 --- a/lib/livebook/notebook/learn.ex +++ b/lib/livebook/notebook/learn.ex @@ -120,7 +120,7 @@ defmodule Livebook.Notebook.Learn do markdown = File.read!(path) # Parse the file to ensure no warnings and read the title. # However, in the info we keep just the file contents to save on memory. - {notebook, %{warnings: warnings}} = Livebook.LiveMarkdown.notebook_from_livemd(markdown) + {notebook, warnings} = Livebook.LiveMarkdown.notebook_from_livemd(markdown) if warnings != [] do items = Enum.map(warnings, &("- " <> &1)) @@ -197,8 +197,7 @@ defmodule Livebook.Notebook.Learn do raise NotFoundError, slug: slug notebook_info -> - {notebook, %{warnings: []}} = - Livebook.LiveMarkdown.notebook_from_livemd(notebook_info.livemd) + {notebook, []} = Livebook.LiveMarkdown.notebook_from_livemd(notebook_info.livemd) {notebook, notebook_info.files} end diff --git a/lib/livebook/teams.ex b/lib/livebook/teams.ex index d27158908..c205d7dd8 100644 --- a/lib/livebook/teams.ex +++ b/lib/livebook/teams.ex @@ -158,7 +158,7 @@ defmodule Livebook.Teams do """ @spec create_hub!(map()) :: Team.t() def create_hub!(attrs) do - changeset = Team.change_hub(%Team{}, attrs) + changeset = Team.change_hub(Team.new(), attrs) team = apply_action!(changeset, :insert) Hubs.save_hub(team) @@ -187,16 +187,16 @@ defmodule Livebook.Teams do @doc """ Encrypts the given value with Teams key derived keys. """ - @spec encrypt_secret_value(String.t(), bitstring(), bitstring()) :: String.t() - def encrypt_secret_value(value, secret, sign_secret) do + @spec encrypt(String.t(), bitstring(), bitstring()) :: String.t() + def encrypt(value, secret, sign_secret) do Plug.Crypto.MessageEncryptor.encrypt(value, secret, sign_secret) end @doc """ Decrypts the given encrypted value with Teams key derived keys. """ - @spec decrypt_secret_value(String.t(), bitstring(), bitstring()) :: {:ok, String.t()} | :error - def decrypt_secret_value(encrypted_value, secret, sign_secret) do + @spec decrypt(String.t(), bitstring(), bitstring()) :: {:ok, String.t()} | :error + def decrypt(encrypted_value, secret, sign_secret) do Plug.Crypto.MessageEncryptor.decrypt(encrypted_value, secret, sign_secret) end diff --git a/lib/livebook/teams/requests.ex b/lib/livebook/teams/requests.ex index fd7e26518..598bfb5fb 100644 --- a/lib/livebook/teams/requests.ex +++ b/lib/livebook/teams/requests.ex @@ -51,7 +51,7 @@ defmodule Livebook.Teams.Requests do {:ok, map()} | {:error, map() | String.t()} | {:transport_error, String.t()} def create_secret(team, secret) do {secret_key, sign_secret} = Teams.derive_keys(team.teams_key) - secret_value = Teams.encrypt_secret_value(secret.value, secret_key, sign_secret) + secret_value = Teams.encrypt(secret.value, secret_key, sign_secret) headers = auth_headers(team) params = %{name: secret.name, value: secret_value} @@ -66,7 +66,7 @@ defmodule Livebook.Teams.Requests do {:ok, map()} | {:error, map() | String.t()} | {:transport_error, String.t()} def update_secret(team, secret) do {secret_key, sign_secret} = Teams.derive_keys(team.teams_key) - secret_value = Teams.encrypt_secret_value(secret.value, secret_key, sign_secret) + secret_value = Teams.encrypt(secret.value, secret_key, sign_secret) headers = auth_headers(team) params = %{name: secret.name, value: secret_value} diff --git a/lib/livebook_web/live/hub/edit/team_component.ex b/lib/livebook_web/live/hub/edit/team_component.ex index dd8a9cabc..fc345f935 100644 --- a/lib/livebook_web/live/hub/edit/team_component.ex +++ b/lib/livebook_web/live/hub/edit/team_component.ex @@ -355,9 +355,23 @@ defmodule LivebookWeb.Hub.Edit.TeamComponent do ENV LIVEBOOK_APPS_PATH_HUB_ID "#{socket.assigns.hub.id}" ENV LIVEBOOK_TEAMS_NAME "#{socket.assigns.hub.hub_name}" ENV LIVEBOOK_TEAMS_OFFLINE_KEY "#{socket.assigns.hub.org_public_key}" + ENV LIVEBOOK_TEAMS_SECRETS "#{encrypt_secrets_to_dockerfile(socket)}" ENV LIVEBOOK_APPS_PATH_WARMUP "manual" RUN /app/bin/warmup_apps.sh\ """) end + + defp encrypt_secrets_to_dockerfile(socket) do + {secret_key, sign_secret} = Livebook.Teams.derive_keys(socket.assigns.hub.teams_key) + + secrets_map = + for %{name: name, value: value} <- socket.assigns.secrets, + into: %{}, + do: {name, value} + + stringified_secrets = Jason.encode!(secrets_map) + + Livebook.Teams.encrypt(stringified_secrets, secret_key, sign_secret) + end end diff --git a/lib/livebook_web/live/open_live.ex b/lib/livebook_web/live/open_live.ex index f86408a94..33ec630b9 100644 --- a/lib/livebook_web/live/open_live.ex +++ b/lib/livebook_web/live/open_live.ex @@ -249,7 +249,7 @@ defmodule LivebookWeb.OpenLive do defp file_from_params(_params), do: Livebook.Settings.default_dir() defp import_source(socket, source, session_opts) do - {notebook, %{warnings: messages}} = Livebook.LiveMarkdown.notebook_from_livemd(source) + {notebook, messages} = Livebook.LiveMarkdown.notebook_from_livemd(source) socket = socket diff --git a/lib/livebook_web/live/session_helpers.ex b/lib/livebook_web/live/session_helpers.ex index ebcf33fee..193604725 100644 --- a/lib/livebook_web/live/session_helpers.ex +++ b/lib/livebook_web/live/session_helpers.ex @@ -115,7 +115,7 @@ defmodule LivebookWeb.SessionHelpers do @spec fork_notebook(Socket.t(), FileSystem.File.t()) :: Socket.t() def fork_notebook(socket, file) do case import_notebook(file) do - {:ok, {notebook, %{warnings: messages}}} -> + {:ok, {notebook, messages}} -> notebook = Livebook.Notebook.forked(notebook) files_dir = Session.files_dir_for_notebook(file) @@ -138,7 +138,7 @@ defmodule LivebookWeb.SessionHelpers do @spec open_notebook(Socket.t(), FileSystem.File.t()) :: Socket.t() def open_notebook(socket, file) do case import_notebook(file) do - {:ok, {notebook, %{warnings: messages}}} -> + {:ok, {notebook, messages}} -> socket |> put_import_warnings(messages) |> create_session(notebook: notebook, file: file, origin: {:file, file}) diff --git a/lib/livebook_web/live/session_live.ex b/lib/livebook_web/live/session_live.ex index ef5d60aef..eca503b3d 100644 --- a/lib/livebook_web/live/session_live.ex +++ b/lib/livebook_web/live/session_live.ex @@ -1845,7 +1845,7 @@ defmodule LivebookWeb.SessionLive do defp open_notebook(socket, origin, fallback_locations, requested_url) do case fetch_content_with_fallbacks(origin, fallback_locations) do {:ok, content} -> - {notebook, %{warnings: messages}} = Livebook.LiveMarkdown.notebook_from_livemd(content) + {notebook, messages} = Livebook.LiveMarkdown.notebook_from_livemd(content) # If the current session has no file, fork the notebook fork? = socket.private.data.file == nil diff --git a/lib/livebook_web/plugs/user_plug.ex b/lib/livebook_web/plugs/user_plug.ex index 7899de1c3..31ffe1d54 100644 --- a/lib/livebook_web/plugs/user_plug.ex +++ b/lib/livebook_web/plugs/user_plug.ex @@ -48,6 +48,8 @@ defmodule LivebookWeb.UserPlug do end end + defp ensure_user_data(conn) when conn.halted, do: conn + defp ensure_user_data(conn) do if Map.has_key?(conn.req_cookies, "lb:user_data") do conn @@ -71,6 +73,8 @@ defmodule LivebookWeb.UserPlug do # Copies user_data from cookie to session, so that it's # accessible to LiveViews + defp mirror_user_data_in_session(conn) when conn.halted, do: conn + defp mirror_user_data_in_session(conn) do user_data = conn.cookies["lb:user_data"] |> Base.decode64!() |> Jason.decode!() put_session(conn, :user_data, user_data) diff --git a/test/livebook/apps_test.exs b/test/livebook/apps_test.exs index 993715373..3f138f471 100644 --- a/test/livebook/apps_test.exs +++ b/test/livebook/apps_test.exs @@ -100,6 +100,54 @@ defmodule Livebook.AppsTest do Livebook.App.close(pid) end + @tag :tmp_dir + test "deploys apps with offline hub secrets", %{tmp_dir: tmp_dir} do + app_path = Path.join(tmp_dir, "app1.livemd") + hub = offline_hub() + + File.write!(app_path, """ + + + # App 2 + + ## Section 1 + + ```elixir + IO.puts("hey") + ``` + + + """) + + Livebook.Apps.subscribe() + + secret = %Livebook.Secrets.Secret{ + name: "DB_PASSWORD", + value: "postgres", + hub_id: hub.id + } + + put_offline_hub_secret(secret) + assert secret in Livebook.Hubs.get_secrets(hub) + + Livebook.Apps.deploy_apps_in_dir(tmp_dir) + + assert_receive {:app_created, %{pid: pid, slug: "offline_hub_app2"}} + + assert_receive {:app_updated, + %{ + slug: "offline_hub_app2", + sessions: [%{app_status: %{execution: :executed}}] + }} + + session_id = Livebook.App.get_session_id(pid) + {:ok, session} = Livebook.Sessions.fetch_session(session_id) + assert %{hub_secrets: [^secret]} = Livebook.Session.get_data(session.pid) + + Livebook.App.close(pid) + remove_offline_hub_secret(secret) + end + @tag :tmp_dir test "skips apps with incomplete config and warns", %{tmp_dir: tmp_dir} do app_path = Path.join(tmp_dir, "app.livemd") diff --git a/test/livebook/live_markdown/import_test.exs b/test/livebook/live_markdown/import_test.exs index 9e2fde7d6..1c2be1567 100644 --- a/test/livebook/live_markdown/import_test.exs +++ b/test/livebook/live_markdown/import_test.exs @@ -62,7 +62,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do ``` """ - {notebook, %{warnings: []}} = Import.notebook_from_livemd(markdown) + {notebook, []} = Import.notebook_from_livemd(markdown) # Match only on the relevant fields as some may be generated (ids). @@ -159,7 +159,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do | Maine | ME | Augusta | """ - {notebook, %{warnings: []}} = Import.notebook_from_livemd(markdown) + {notebook, []} = Import.notebook_from_livemd(markdown) assert %Notebook{ name: "My Notebook", @@ -188,7 +188,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do Some markdown. """ - {notebook, %{warnings: []}} = Import.notebook_from_livemd(markdown) + {notebook, []} = Import.notebook_from_livemd(markdown) assert %Notebook{ name: "Untitled notebook", @@ -213,7 +213,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do ###### Tiny heading """ - {notebook, %{warnings: messages}} = Import.notebook_from_livemd(markdown) + {notebook, messages} = Import.notebook_from_livemd(markdown) assert %Notebook{ name: "Untitled notebook", @@ -257,7 +257,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do ## # Section """ - {notebook, %{warnings: []}} = Import.notebook_from_livemd(markdown) + {notebook, []} = Import.notebook_from_livemd(markdown) assert %Notebook{ name: "My *Notebook*", @@ -278,7 +278,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do ## Actual section """ - {notebook, %{warnings: []}} = Import.notebook_from_livemd(markdown) + {notebook, []} = Import.notebook_from_livemd(markdown) assert %Notebook{ name: "My Notebook", @@ -307,7 +307,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do ``` """ - {notebook, %{warnings: []}} = Import.notebook_from_livemd(markdown) + {notebook, []} = Import.notebook_from_livemd(markdown) assert %Notebook{ name: "Untitled notebook", @@ -337,7 +337,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do Some markdown. """ - {notebook, %{warnings: messages}} = Import.notebook_from_livemd(markdown) + {notebook, messages} = Import.notebook_from_livemd(markdown) assert %Notebook{ name: "My Notebook", @@ -369,7 +369,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do Some markdown. """ - {_notebook, %{warnings: messages}} = Import.notebook_from_livemd(markdown) + {_notebook, messages} = Import.notebook_from_livemd(markdown) assert ["line 3 - closing unclosed backquotes ` at end of input"] == messages end @@ -395,7 +395,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do ``` """ - {notebook, %{warnings: []}} = Import.notebook_from_livemd(markdown) + {notebook, []} = Import.notebook_from_livemd(markdown) assert %Notebook{ name: "My Notebook", @@ -451,7 +451,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do ``` """ - {notebook, %{warnings: []}} = Import.notebook_from_livemd(markdown) + {notebook, []} = Import.notebook_from_livemd(markdown) assert %Notebook{ name: "My Notebook", @@ -499,7 +499,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do Cell 2 """ - {notebook, %{warnings: []}} = Import.notebook_from_livemd(markdown) + {notebook, []} = Import.notebook_from_livemd(markdown) assert %Notebook{ name: "My Notebook", @@ -543,7 +543,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do Cell 1 """ - {notebook, %{warnings: []}} = Import.notebook_from_livemd(markdown) + {notebook, []} = Import.notebook_from_livemd(markdown) assert %Notebook{ name: "My Notebook", @@ -581,7 +581,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do Cell 1 """ - {notebook, %{warnings: messages}} = Import.notebook_from_livemd(markdown) + {notebook, messages} = Import.notebook_from_livemd(markdown) assert %Notebook{ name: "My Notebook", @@ -630,7 +630,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do ``` """ - {notebook, %{warnings: []}} = Import.notebook_from_livemd(markdown) + {notebook, []} = Import.notebook_from_livemd(markdown) assert %Notebook{ name: "My Notebook", @@ -676,7 +676,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do ``` """ - {notebook, %{warnings: []}} = Import.notebook_from_livemd(markdown) + {notebook, []} = Import.notebook_from_livemd(markdown) assert %Notebook{ name: "My Notebook", @@ -715,7 +715,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do # My Notebook """ - {notebook, %{warnings: []}} = Import.notebook_from_livemd(markdown) + {notebook, []} = Import.notebook_from_livemd(markdown) assert %Notebook{name: "My Notebook", persist_outputs: true} = notebook end @@ -728,7 +728,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do # My Notebook """ - {notebook, %{warnings: []}} = Import.notebook_from_livemd(markdown) + {notebook, []} = Import.notebook_from_livemd(markdown) assert %Notebook{name: "My Notebook", autosave_interval_s: 10} = notebook end @@ -740,7 +740,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do # My Notebook """ - {notebook, %{warnings: []}} = Import.notebook_from_livemd(markdown) + {notebook, []} = Import.notebook_from_livemd(markdown) assert %Notebook{name: "My Notebook", default_language: :erlang} = notebook end @@ -754,7 +754,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do # My Notebook """ - {notebook, %{warnings: []}} = Import.notebook_from_livemd(markdown) + {notebook, []} = Import.notebook_from_livemd(markdown) assert %Notebook{name: "My Notebook", hub_id: ^hub_id} = notebook end @@ -766,7 +766,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do # My Notebook """ - {notebook, %{warnings: messages}} = Import.notebook_from_livemd(markdown) + {notebook, messages} = Import.notebook_from_livemd(markdown) assert messages == ["ignoring notebook Hub with unknown id"] assert notebook.hub_id != "nonexistent" @@ -780,7 +780,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do # My Notebook """ - {notebook, %{warnings: []}} = Import.notebook_from_livemd(markdown) + {notebook, []} = Import.notebook_from_livemd(markdown) assert %Notebook{ name: "My Notebook", @@ -804,7 +804,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do # My Notebook """ - {notebook, %{warnings: []}} = Import.notebook_from_livemd(markdown) + {notebook, []} = Import.notebook_from_livemd(markdown) assert %Notebook{ name: "My Notebook", @@ -823,7 +823,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do """ - {_notebook, %{warnings: messages}} = Import.notebook_from_livemd(markdown) + {_notebook, messages} = Import.notebook_from_livemd(markdown) assert [ "found an input cell, but those are no longer supported, please use Kino.Input instead" @@ -839,7 +839,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do """ - {_notebook, %{warnings: messages}} = Import.notebook_from_livemd(markdown) + {_notebook, messages} = Import.notebook_from_livemd(markdown) assert [ "found an input cell, but those are no longer supported, please use Kino.Input instead." <> @@ -870,7 +870,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do ``` """ - {notebook, %{warnings: []}} = Import.notebook_from_livemd(markdown) + {notebook, []} = Import.notebook_from_livemd(markdown) assert %Notebook{ name: "My Notebook", @@ -902,7 +902,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do ![](images/dog.jpeg) """ - {_notebook, %{warnings: messages}} = Import.notebook_from_livemd(markdown) + {_notebook, messages} = Import.notebook_from_livemd(markdown) assert [ "found Markdown images pointing to the images/ directory. Using this directory has been deprecated, please use notebook files instead" @@ -929,7 +929,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do ``` """ - {notebook, %{warnings: messages}} = Import.notebook_from_livemd(markdown) + {notebook, messages} = Import.notebook_from_livemd(markdown) assert %Notebook{ name: "My Notebook", @@ -961,7 +961,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do ``` """ - {notebook, %{warnings: messages}} = Import.notebook_from_livemd(markdown) + {notebook, messages} = Import.notebook_from_livemd(markdown) assert %Notebook{ name: "My Notebook", @@ -1003,7 +1003,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do ``` """ - {notebook, %{warnings: messages}} = Import.notebook_from_livemd(markdown) + {notebook, messages} = Import.notebook_from_livemd(markdown) assert %Notebook{ name: "My Notebook", @@ -1040,7 +1040,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do ## Section 1 """ - {notebook, %{warnings: []}} = Import.notebook_from_livemd(markdown) + {notebook, []} = Import.notebook_from_livemd(markdown) assert %Notebook{ name: "My Notebook", @@ -1067,7 +1067,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do ``` """ - {notebook, %{warnings: []}} = Import.notebook_from_livemd(markdown) + {notebook, []} = Import.notebook_from_livemd(markdown) assert %Notebook{ name: "My Notebook", @@ -1120,7 +1120,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do """ - {notebook, %{warnings: []}} = Import.notebook_from_livemd(markdown) + {notebook, []} = Import.notebook_from_livemd(markdown) assert %Notebook{hub_secret_names: ["DB_PASSWORD"]} = notebook end @@ -1138,7 +1138,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do """ - {notebook, %{warnings: messages}} = Import.notebook_from_livemd(markdown) + {notebook, messages} = Import.notebook_from_livemd(markdown) assert %Notebook{hub_secret_names: []} = notebook assert messages == ["failed to verify notebook stamp"] @@ -1159,10 +1159,10 @@ defmodule Livebook.LiveMarkdown.ImportTest do """ - {notebook, %{warnings: []}} = Import.notebook_from_livemd(markdown) + {notebook, []} = Import.notebook_from_livemd(markdown) assert %Notebook{ - hub_id: "personal-hub", + hub_id: "team-org-number-3079", hub_secret_names: ["DB_PASSWORD"], teams_enabled: true } = notebook @@ -1183,9 +1183,9 @@ defmodule Livebook.LiveMarkdown.ImportTest do """ - {notebook, %{warnings: messages}} = Import.notebook_from_livemd(markdown) + {notebook, messages} = Import.notebook_from_livemd(markdown) - assert %Notebook{hub_id: "personal-hub", teams_enabled: false} = notebook + assert %Notebook{hub_id: "team-org-number-3079", teams_enabled: true} = notebook assert messages == ["failed to verify notebook stamp"] end @@ -1206,7 +1206,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do """ - {notebook, %{warnings: [_]}} = Import.notebook_from_livemd(markdown) + {notebook, [_]} = Import.notebook_from_livemd(markdown) assert %Notebook{hub_id: ^hub_id, teams_enabled: true} = notebook end @@ -1220,7 +1220,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do # My Notebook """ - {notebook, %{warnings: []}} = Import.notebook_from_livemd(markdown) + {notebook, []} = Import.notebook_from_livemd(markdown) assert %Notebook{ file_entries: [ @@ -1245,7 +1245,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do # My Notebook """ - {notebook, %{warnings: messages}} = Import.notebook_from_livemd(markdown) + {notebook, messages} = Import.notebook_from_livemd(markdown) assert %Notebook{file_entries: []} = notebook @@ -1259,7 +1259,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do # My Notebook """ - {notebook, %{warnings: []}} = Import.notebook_from_livemd(markdown) + {notebook, []} = Import.notebook_from_livemd(markdown) assert %Notebook{ file_entries: [ @@ -1327,7 +1327,7 @@ defmodule Livebook.LiveMarkdown.ImportTest do } |> Livebook.LiveMarkdown.Export.notebook_to_livemd() - {notebook, %{warnings: []}} = Import.notebook_from_livemd(markdown) + {notebook, []} = Import.notebook_from_livemd(markdown) assert %Notebook{ file_entries: [ diff --git a/test/livebook_teams/apps_test.exs b/test/livebook_teams/apps_test.exs new file mode 100644 index 000000000..6a98c005f --- /dev/null +++ b/test/livebook_teams/apps_test.exs @@ -0,0 +1,48 @@ +defmodule Livebook.Integration.AppsTest do + use Livebook.TeamsIntegrationCase, async: true + + describe "deploy_apps_in_dir/1" do + @tag :tmp_dir + test "deploys apps with hub secrets", %{user: user, node: node, tmp_dir: tmp_dir} do + hub = create_team_hub(user, node) + hub_id = hub.id + app_path = Path.join(tmp_dir, "app.livemd") + secret = insert_secret(name: "DB_PASSWORD", value: "postgres", hub_id: hub.id) + secret_name = secret.name + + markdown = """ + + + # Team Hub + + ## Fetch Env Var + + ```elixir + System.fetch_env!("LB_#{secret.name}") + ``` + """ + + {notebook, []} = Livebook.LiveMarkdown.notebook_from_livemd(markdown) + notebook = Map.replace!(notebook, :hub_secret_names, [secret_name]) + {source, []} = Livebook.LiveMarkdown.notebook_to_livemd(notebook) + + File.write!(app_path, source) + + Livebook.Apps.subscribe() + Livebook.Apps.deploy_apps_in_dir(tmp_dir) + + assert_receive {:app_created, %{pid: pid, slug: ^hub_id}} + + assert_receive {:app_updated, + %{slug: ^hub_id, sessions: [%{app_status: %{execution: :executed}}]}} + + session_id = Livebook.App.get_session_id(pid) + {:ok, session} = Livebook.Sessions.fetch_session(session_id) + + assert %{notebook: %{hub_secret_names: [^secret_name]}, hub_secrets: [^secret]} = + Livebook.Session.get_data(session.pid) + + Livebook.App.close(pid) + end + end +end diff --git a/test/livebook_teams/hubs/team_client_test.exs b/test/livebook_teams/hubs/team_client_test.exs index 189ce357d..2af352517 100644 --- a/test/livebook_teams/hubs/team_client_test.exs +++ b/test/livebook_teams/hubs/team_client_test.exs @@ -3,8 +3,6 @@ defmodule Livebook.Hubs.TeamClientTest do alias Livebook.Hubs.TeamClient - import Livebook.HubHelpers - @moduletag :capture_log setup do diff --git a/test/livebook_teams/hubs/team_test.exs b/test/livebook_teams/hubs/team_test.exs index 130f5effe..8274d0842 100644 --- a/test/livebook_teams/hubs/team_test.exs +++ b/test/livebook_teams/hubs/team_test.exs @@ -7,21 +7,7 @@ defmodule Livebook.Hubs.TeamTest do describe "stamping" do test "generates and verifies stamp for a notebook", %{user: user, node: node} do - org = :erpc.call(node, Hub.Integration, :create_org, []) - org_key = :erpc.call(node, Hub.Integration, :create_org_key, [[org: org]]) - org_key_pair = :erpc.call(node, Hub.Integration, :create_org_key_pair, [[org: org]]) - token = :erpc.call(node, Hub.Integration, :associate_user_with_org, [user, org]) - - team = - build(:team, - id: "team-#{org.name}", - hub_name: org.name, - user_id: user.id, - org_id: org.id, - org_key_id: org_key.id, - org_public_key: org_key_pair.public_key, - session_token: token - ) + team = create_team_hub(user, node) notebook_source = """ # Team notebook diff --git a/test/livebook_teams/teams/connection_test.exs b/test/livebook_teams/teams/connection_test.exs index 2540d5468..a5856be08 100644 --- a/test/livebook_teams/teams/connection_test.exs +++ b/test/livebook_teams/teams/connection_test.exs @@ -5,8 +5,6 @@ defmodule Livebook.Teams.ConnectionTest do alias Livebook.Teams.Connection - import Livebook.HubHelpers - describe "connect" do test "successfully authenticates the websocket connection", %{user: user, node: node} do {_, headers} = build_team_headers(user, node) diff --git a/test/livebook_teams/web/hub/edit_live_test.exs b/test/livebook_teams/web/hub/edit_live_test.exs index 4b8c5ea1a..4351b54b1 100644 --- a/test/livebook_teams/web/hub/edit_live_test.exs +++ b/test/livebook_teams/web/hub/edit_live_test.exs @@ -2,22 +2,21 @@ defmodule LivebookWeb.Integration.Hub.EditLiveTest do use Livebook.TeamsIntegrationCase, async: true import Phoenix.LiveViewTest - import Livebook.HubHelpers import Livebook.TestHelpers alias Livebook.Hubs + setup %{user: user, node: node} do + Livebook.Hubs.subscribe([:crud, :connection, :secrets]) + hub = create_team_hub(user, node) + id = hub.id + + assert_receive {:hub_connected, ^id} + + {:ok, hub: hub} + end + describe "team" do - setup %{user: user, node: node} do - Livebook.Hubs.subscribe([:crud, :connection, :secrets]) - hub = create_team_hub(user, node) - id = hub.id - - assert_receive {:hub_connected, ^id} - - {:ok, hub: hub} - end - test "updates the hub", %{conn: conn, hub: hub} do {:ok, view, _html} = live(conn, ~p"/hub/#{hub.id}") diff --git a/test/livebook_teams/web/hub/new_live_test.exs b/test/livebook_teams/web/hub/new_live_test.exs index eab6bb767..f8759852c 100644 --- a/test/livebook_teams/web/hub/new_live_test.exs +++ b/test/livebook_teams/web/hub/new_live_test.exs @@ -3,7 +3,6 @@ defmodule LivebookWeb.Hub.NewLiveTest do alias Livebook.Teams.Org - import Livebook.HubHelpers import Phoenix.LiveViewTest test "render hub selection cards", %{conn: conn} do diff --git a/test/livebook_teams/web/session_live_test.exs b/test/livebook_teams/web/session_live_test.exs index 1a199c8eb..1f18c445b 100644 --- a/test/livebook_teams/web/session_live_test.exs +++ b/test/livebook_teams/web/session_live_test.exs @@ -2,7 +2,6 @@ defmodule LivebookWeb.Integration.SessionLiveTest do use Livebook.TeamsIntegrationCase, async: true import Phoenix.LiveViewTest - import Livebook.HubHelpers import Livebook.SessionHelpers alias Livebook.{Sessions, Session} diff --git a/test/support/factory.ex b/test/support/factory.ex index 6d1c096b1..42f4340cc 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -25,7 +25,8 @@ defmodule Livebook.Factory do org_key_id: 1, org_public_key: Livebook.Utils.random_id(), teams_key: org.teams_key, - session_token: Livebook.Utils.random_short_id() + session_token: Livebook.Utils.random_short_id(), + offline: nil } end diff --git a/test/support/hub_helpers.ex b/test/support/hub_helpers.ex index 995401c61..3a357ce8b 100644 --- a/test/support/hub_helpers.ex +++ b/test/support/hub_helpers.ex @@ -10,7 +10,14 @@ defmodule Livebook.HubHelpers do teams_key: "A9TarFeAzmX3sDwSPm5JP5qbLPnNpLpzmjVZUCHXwmI", org_public_key: "MIIBCgKCAQEA5v_qciaRGOZd5kgCQbhQDgFCnTnIKI5xzN4m4rVtLXMPH7RTA-K6C-e4wy2gn8zulXgSYX4vXDACSjFAG4PlFhXTPgb-v3rFLwbBrUHdaTMTyxRdK52NyNoDpYklQ7FaEU9vr3Z_-cpAQjdADOV1k45GmFe3bo4gImIfUSDYp1rRiEsYcIBt0Wc0S-vQHKSlmfcCexe254_UkvWjLW7KO790bem-PSWcBI_713oRr2mQoxXeeGKd5dSyFsIr5SZXVRWcRK3soQimCXB0ddBSXZ7d2Md3P9Ylo7TcYdBGHlwVIsrmB-P70KPHPYuAVgS9QsIiiMGXPwYVW77xNRTlcwIDAQAB", - hub_name: "org-number-3079" + hub_name: "org-number-3079", + user_id: 0, + org_id: 0, + org_key_id: 0, + session_token: "", + offline: %Livebook.Hubs.Team.Offline{ + secrets: [] + } } def create_team_hub(user, node) do @@ -67,11 +74,38 @@ defmodule Livebook.HubHelpers do end def set_offline_hub() do - Livebook.Hubs.set_offline_hub(@offline_hub) + hub = offline_hub() + ^hub = Livebook.Hubs.save_hub(hub) + + hub end def offline_hub(), do: @offline_hub + def put_offline_hub_secret(secret) do + hub = offline_hub() + {:ok, pid} = hub_pid(hub) + {secret_key, sign_secret} = Livebook.Teams.derive_keys(hub.teams_key) + value = Livebook.Teams.encrypt(secret.value, secret_key, sign_secret) + secret_created = LivebookProto.SecretCreated.new(name: secret.name, value: value) + + send(pid, {:event, :secret_created, secret_created}) + end + + def remove_offline_hub_secret(secret) do + hub = offline_hub() + {:ok, pid} = hub_pid(hub) + secret_deleted = LivebookProto.SecretDeleted.new(name: secret.name) + + send(pid, {:event, :secret_deleted, secret_deleted}) + end + + defp hub_pid(hub) do + if pid = GenServer.whereis({:via, Registry, {Livebook.HubsRegistry, hub.id}}) do + {:ok, pid} + end + end + defp hub_element_id(id), do: "#hubs #hub-#{id}" defp erpc_call(node, fun, args) do diff --git a/test/support/teams_integration_case.ex b/test/support/teams_integration_case.ex index 578e6dc23..cc6309782 100644 --- a/test/support/teams_integration_case.ex +++ b/test/support/teams_integration_case.ex @@ -11,6 +11,8 @@ defmodule Livebook.TeamsIntegrationCase do @moduletag :teams_integration alias Livebook.TeamsServer + + import Livebook.HubHelpers end end