mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-11-08 05:04:46 +08:00
Load secrets from Hub when deploying an App (#2098)
This commit is contained in:
parent
a8ae19b9cf
commit
3ecc0b3653
30 changed files with 379 additions and 206 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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, """
|
||||
<!-- livebook:{"app_settings":{"slug":"offline_hub_app2"},"hub_id":"team-org-number-3079"} -->
|
||||
|
||||
# App 2
|
||||
|
||||
## Section 1
|
||||
|
||||
```elixir
|
||||
IO.puts("hey")
|
||||
```
|
||||
|
||||
<!-- livebook:{"offset":148,"stamp":{"token":"QTEyOEdDTQ.M486X5ISDg_wVwvndrMYJKIkfXa5qAwggh5LzkV41wVOY8SNQvfA4EFx4Tk.RrcteIrzJVrqQdhH.mtmu5KFj.bibmp2rxZoniYX1xY8dTvw","token_signature":"28ucTCoDXxahOIMg7SvdYIoLpGIUSahEa7mchH0jKncKeZH8-w-vOaL1F1uj_94lqQJFkmHWv988__1bPmYCorw7F1wAvAaprt3o2vSitWWmBszuF5JaimkFqOFcK3mc4NHuswQKuBjSL0W_yR-viiwlx6zPNsTpftVKjRI2Cri1PsMeZgahfdR2gy1OEgavzU6J6YWsNQHIMWgt5gwT6fIua1zaV7K8-TA6-6NRgcfG-pSJqRIm-3-vnbH5lkXRCgXCo_S9zWa6Jrcl5AbLObSr5DUueiwac1RobH7jNghCm1F-o1cUk9W-BJRZ7igVMwaYqLaOnKO8ya9CrkIiMg","version":1}} -->
|
||||
""")
|
||||
|
||||
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")
|
||||
|
|
|
|||
|
|
@ -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
|
|||
<!-- livebook:{"livebook_object":"cell_input","name":"length","type":"text","value":"100"} -->
|
||||
"""
|
||||
|
||||
{_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
|
|||
<!-- livebook:{"livebook_object":"cell_input","name":"length","reactive":true,"type":"text","value":"100"} -->
|
||||
"""
|
||||
|
||||
{_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
|
|||

|
||||
"""
|
||||
|
||||
{_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
|
|||
<!-- livebook:{"offset":58,"stamp":{"token":"QTEyOEdDTQ.LF8LTeMYrtq8S7wsKMmk2YgOQzMAkEKT2d8fq1Gz3Ot1mydOgEZ1B4hcEZc.Wec6NwBQ584kE661.a_N-5jDiWrjhHha9zxHQ6JJOmxeqgiya3m6YlKt1Na_DPnEfXyLnengaUzQSrf8.ZoD5r6-H87RpTyvFkvEOQw","version":1}} -->
|
||||
"""
|
||||
|
||||
{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
|
|||
<!-- livebook:{"offset":58,"stamp":{"token":"invalid","version":1}} -->
|
||||
"""
|
||||
|
||||
{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
|
|||
<!-- livebook:{"offset":111,"stamp":{"token":"QTEyOEdDTQ.VezIVId8jnwawq79Mrxfx-bCeatRPQhg6zRw4gHjfGC_SimS5WSbdETLVnc.IXXzTiDf3JRLiNGt.rtLJ97RxNYLeF_x-2AigBt_y0bYdlh30bnEKUO7FKfJL-9EB6EQOgr1WbdHTkw.rU1OsjOhfU48tE3s0GB6Ag","token_signature":"M_LZP7ssEdLiVqo8_bZYFtJYAzqV5wczQF1mUdaOZeTYoCYXeLA9VuEajMCOwiVgMjfG24Y4PHrnABGUj0bB74xgEMOLtb6Ark42pCJfGUDLXpB2azMnwY6VRXi1nBUTnb_-MajpJVF_MgWGuj0cxyQ4rlfBu2VK1FQn8JGopLKBD7LcICZ9wkLJw3H_lXGTbVxnS6tLcvJZQdLJccTe4edpdF-_uG-d1KqtJva50jZ9vevyxh6zUubl0k5JnA3SUaTpc7AEsqmAMx9qQsoR64P-VobSFTKuuOFrlQsq21OupPCRKmUBU-RKrBvHbc6Fa1eoQigEQom5droj9cASdw","version":1}} -->
|
||||
"""
|
||||
|
||||
{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
|
|||
<!-- livebook:{"offset":58,"stamp":{"token":"invalid","token_signature":"invalid","version":1}} -->
|
||||
"""
|
||||
|
||||
{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
|
|||
<!-- livebook:{"offset":58,"stamp":{"token":"invalid","token_signature":"invalid","version":1}} -->
|
||||
"""
|
||||
|
||||
{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: [
|
||||
|
|
|
|||
48
test/livebook_teams/apps_test.exs
Normal file
48
test/livebook_teams/apps_test.exs
Normal file
|
|
@ -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 = """
|
||||
<!-- livebook:{"app_settings":{"slug":"#{hub_id}"},"hub_id":"#{hub_id}"} -->
|
||||
|
||||
# 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
|
||||
|
|
@ -3,8 +3,6 @@ defmodule Livebook.Hubs.TeamClientTest do
|
|||
|
||||
alias Livebook.Hubs.TeamClient
|
||||
|
||||
import Livebook.HubHelpers
|
||||
|
||||
@moduletag :capture_log
|
||||
|
||||
setup do
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ defmodule Livebook.TeamsIntegrationCase do
|
|||
@moduletag :teams_integration
|
||||
|
||||
alias Livebook.TeamsServer
|
||||
|
||||
import Livebook.HubHelpers
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue