mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-05 04:24:21 +08:00
Add stamp to notebook source and persist allowed hub secrets (#1768)
This commit is contained in:
parent
ae797040cd
commit
7f71f2fe9f
21 changed files with 542 additions and 200 deletions
|
@ -52,12 +52,11 @@ defmodule Livebook.Application do
|
|||
|
||||
case Supervisor.start_link(children, opts) do
|
||||
{:ok, _} = result ->
|
||||
Livebook.Migration.migrate()
|
||||
load_lb_env_vars()
|
||||
clear_env_vars()
|
||||
display_startup_info()
|
||||
insert_personal_hub()
|
||||
Livebook.Hubs.connect_hubs()
|
||||
migrate_secrets()
|
||||
deploy_apps()
|
||||
result
|
||||
|
||||
|
@ -214,31 +213,6 @@ defmodule Livebook.Application do
|
|||
defp app_specs, do: []
|
||||
end
|
||||
|
||||
defp insert_personal_hub do
|
||||
unless Livebook.Hubs.hub_exists?(Livebook.Hubs.Personal.id()) do
|
||||
Livebook.Hubs.save_hub(%Livebook.Hubs.Personal{
|
||||
id: Livebook.Hubs.Personal.id(),
|
||||
hub_name: "My Hub",
|
||||
hub_emoji: "🏠"
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Remove in the future
|
||||
defp migrate_secrets do
|
||||
for %{name: name, value: value} <- Livebook.Storage.all(:secrets) do
|
||||
secret = %Livebook.Secrets.Secret{
|
||||
name: name,
|
||||
value: value,
|
||||
hub_id: Livebook.Hubs.Personal.id(),
|
||||
readonly: false
|
||||
}
|
||||
|
||||
Livebook.Secrets.set_secret(secret)
|
||||
Livebook.Storage.delete(:secrets, name)
|
||||
end
|
||||
end
|
||||
|
||||
defp deploy_apps() do
|
||||
if apps_path = Livebook.Config.apps_path() do
|
||||
Livebook.Apps.deploy_apps_in_dir(apps_path, password: Livebook.Config.apps_path_password())
|
||||
|
|
|
@ -246,6 +246,24 @@ defmodule Livebook.Hubs do
|
|||
Provider.delete_secret(hub, secret)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates a notebook stamp.
|
||||
"""
|
||||
@spec notebook_stamp(Provider.t(), iodata(), map()) ::
|
||||
{:ok, Provider.notebook_stamp()} | :skip | :error
|
||||
def notebook_stamp(hub, notebook_source, metadata) do
|
||||
Provider.notebook_stamp(hub, notebook_source, metadata)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Verifies a notebook stamp and returns the decrypted metadata.
|
||||
"""
|
||||
@spec verify_notebook_stamp(Provider.t(), iodata(), Provider.notebook_stamp()) ::
|
||||
{:ok, metadata :: map()} | :error
|
||||
def verify_notebook_stamp(hub, notebook_source, stamp) do
|
||||
Provider.verify_notebook_stamp(hub, notebook_source, stamp)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks the hub capability for given hub.
|
||||
"""
|
||||
|
|
|
@ -185,4 +185,11 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Enterprise do
|
|||
reason = EnterpriseClient.get_connection_error(enterprise.id)
|
||||
"Cannot connect to Hub: #{reason}. Will attempt to reconnect automatically..."
|
||||
end
|
||||
|
||||
# TODO: implement signing through the enterprise server
|
||||
def notebook_stamp(_hub, _notebook_source, _metadata) do
|
||||
:skip
|
||||
end
|
||||
|
||||
def verify_notebook_stamp(_hub, _notebook_source, _stamp), do: raise("not implemented")
|
||||
end
|
||||
|
|
|
@ -162,4 +162,10 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Fly do
|
|||
def delete_secret(_fly, _secret), do: :ok
|
||||
|
||||
def connection_error(_fly), do: raise("not implemented")
|
||||
|
||||
def notebook_stamp(_hub, _notebook_source, _metadata) do
|
||||
:skip
|
||||
end
|
||||
|
||||
def verify_notebook_stamp(_hub, _notebook_source, _stamp), do: raise("not implemented")
|
||||
end
|
||||
|
|
|
@ -9,15 +9,17 @@ defmodule Livebook.Hubs.Personal do
|
|||
@type t :: %__MODULE__{
|
||||
id: String.t() | nil,
|
||||
hub_name: String.t() | nil,
|
||||
hub_emoji: String.t() | nil
|
||||
hub_emoji: String.t() | nil,
|
||||
secret_key: String.t() | nil
|
||||
}
|
||||
|
||||
embedded_schema do
|
||||
field :hub_name, :string
|
||||
field :hub_emoji, :string
|
||||
field :secret_key, :string, redact: true
|
||||
end
|
||||
|
||||
@fields ~w(hub_name hub_emoji)a
|
||||
@fields ~w(hub_name hub_emoji secret_key)a
|
||||
|
||||
@doc """
|
||||
The personal hub fixed id.
|
||||
|
@ -83,6 +85,14 @@ defmodule Livebook.Hubs.Personal do
|
|||
def set_startup_secrets(secrets) do
|
||||
:persistent_term.put(@secret_startup_key, secrets)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates a random secret key used for stamping the notebook.
|
||||
"""
|
||||
@spec generate_secret_key() :: String.t()
|
||||
def generate_secret_key() do
|
||||
:crypto.strong_rand_bytes(64) |> Base.url_encode64(padding: false)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Personal do
|
||||
|
@ -90,7 +100,13 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Personal do
|
|||
alias Livebook.Secrets
|
||||
|
||||
def load(personal, fields) do
|
||||
%{personal | id: fields.id, hub_name: fields.hub_name, hub_emoji: fields.hub_emoji}
|
||||
%{
|
||||
personal
|
||||
| id: fields.id,
|
||||
hub_name: fields.hub_name,
|
||||
hub_emoji: fields.hub_emoji,
|
||||
secret_key: fields.secret_key
|
||||
}
|
||||
end
|
||||
|
||||
def to_metadata(personal) do
|
||||
|
@ -131,4 +147,51 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Personal do
|
|||
end
|
||||
|
||||
def connection_error(_personal), do: raise("not implemented")
|
||||
|
||||
def notebook_stamp(_hub, _notebook_source, metadata) when metadata == %{} do
|
||||
:skip
|
||||
end
|
||||
|
||||
def notebook_stamp(hub, notebook_source, metadata) do
|
||||
# We use AES-GCM-128 to encrypt metadata and generate signature
|
||||
# for both metadata and the notebook source. We make use of the
|
||||
# implementation in MessageEncryptor, which conveniently returns
|
||||
# a single token
|
||||
|
||||
{secret, sign_secret} = derive_keys(hub.secret_key)
|
||||
|
||||
payload = :erlang.term_to_binary(metadata)
|
||||
token = Plug.Crypto.MessageEncryptor.encrypt(payload, notebook_source, secret, sign_secret)
|
||||
|
||||
stamp = %{"version" => 1, "token" => token}
|
||||
|
||||
{:ok, stamp}
|
||||
end
|
||||
|
||||
def verify_notebook_stamp(hub, notebook_source, stamp) do
|
||||
%{"version" => 1, "token" => token} = stamp
|
||||
|
||||
{secret, sign_secret} = derive_keys(hub.secret_key)
|
||||
|
||||
case Plug.Crypto.MessageEncryptor.decrypt(token, notebook_source, secret, sign_secret) do
|
||||
{:ok, payload} ->
|
||||
metadata = Plug.Crypto.non_executable_binary_to_term(payload)
|
||||
{:ok, metadata}
|
||||
|
||||
:error ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
defp derive_keys(secret_key) do
|
||||
binary_key = Base.url_decode64!(secret_key, padding: false)
|
||||
|
||||
<<secret::16-bytes, sign_secret::16-bytes>> =
|
||||
Plug.Crypto.KeyGenerator.generate(binary_key, "notebook signing",
|
||||
length: 32,
|
||||
cache: Plug.Crypto.Keys
|
||||
)
|
||||
|
||||
{secret, sign_secret}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,10 +3,28 @@ defprotocol Livebook.Hubs.Provider do
|
|||
|
||||
alias Livebook.Secrets.Secret
|
||||
|
||||
@type t :: Livebook.Hubs.Enterprise.t() | Livebook.Hubs.Fly.t() | Livebook.Hubs.Personal.t()
|
||||
@type capability :: :connect | :list_secrets | :create_secret
|
||||
|
||||
@type capabilities :: list(capability())
|
||||
@type changeset_errors :: %{required(:errors) => list({String.t(), {Stirng.t(), list()}})}
|
||||
|
||||
@type changeset_errors :: %{required(:errors) => list({String.t(), {String.t(), list()}})}
|
||||
|
||||
@typedoc """
|
||||
An provider-specific map stored as notebook stamp.
|
||||
|
||||
Notebook stamp is meant to serve two purposes:
|
||||
|
||||
1. Signing notebook source to ensure notebook integrity and
|
||||
authenticity.
|
||||
|
||||
2. Storing sensitive notebook metadata in encrypted form, so that
|
||||
it cannot be read nor modified outside of Livebook.
|
||||
|
||||
Those can be achieved using arbitrary cryptography mechanics. The
|
||||
stamp itself is an opaque map, however it must be JSON-compatible
|
||||
and use string keys.
|
||||
"""
|
||||
@type notebook_stamp :: map()
|
||||
|
||||
@doc """
|
||||
Transforms given hub to `Livebook.Hubs.Metadata` struct.
|
||||
|
@ -73,4 +91,21 @@ defprotocol Livebook.Hubs.Provider do
|
|||
"""
|
||||
@spec connection_error(t()) :: String.t() | nil
|
||||
def connection_error(hub)
|
||||
|
||||
@doc """
|
||||
Generates a notebook stamp.
|
||||
|
||||
See `t:notebook_stamp/0` for more details.
|
||||
"""
|
||||
@spec notebook_stamp(t(), iodata(), map()) :: {:ok, notebook_stamp()} | :skip | :error
|
||||
def notebook_stamp(hub, notebook_source, metadata)
|
||||
|
||||
@doc """
|
||||
Verifies a notebook stamp and returns the decrypted metadata.
|
||||
|
||||
See `t:notebook_stamp/0` for more details.
|
||||
"""
|
||||
@spec verify_notebook_stamp(t(), iodata(), notebook_stamp()) ::
|
||||
{:ok, metadata :: map()} | :error
|
||||
def verify_notebook_stamp(hub, notebook_source, stamp)
|
||||
end
|
||||
|
|
|
@ -23,10 +23,12 @@ defmodule Livebook.LiveMarkdown do
|
|||
# 5. Comments of the form `<!-- livebook:json_object -->` hold Livebook data
|
||||
# and may be one of the following:
|
||||
#
|
||||
# * a description of a notebook object that cannot be naturally encoded
|
||||
# * description of a notebook object that cannot be naturally encoded
|
||||
# using Markdown, in such case the JSON contains a "livebook_object" field
|
||||
#
|
||||
# * a metadata that may appear anywhere and applies to the element
|
||||
# * notebook stamp data with `"stamp"` and `"offset"` fields
|
||||
#
|
||||
# * metadata that may appear anywhere and applies to the element
|
||||
# it directly precedes, recognised metadatas are:
|
||||
#
|
||||
# - `{"force_markdown":true}` - an annotation forcing the next Markdown
|
||||
|
|
|
@ -11,8 +11,13 @@ defmodule Livebook.LiveMarkdown.Export do
|
|||
ctx = %{include_outputs?: include_outputs?, js_ref_with_data: js_ref_with_data}
|
||||
|
||||
iodata = render_notebook(notebook, ctx)
|
||||
|
||||
# Add trailing newline
|
||||
IO.iodata_to_binary([iodata, "\n"])
|
||||
notebook_source = [iodata, "\n"]
|
||||
|
||||
notebook_footer = render_notebook_footer(notebook, notebook_source)
|
||||
|
||||
IO.iodata_to_binary([notebook_source, notebook_footer])
|
||||
end
|
||||
|
||||
defp collect_js_output_data(notebook) do
|
||||
|
@ -68,7 +73,7 @@ defmodule Livebook.LiveMarkdown.Export do
|
|||
end
|
||||
|
||||
defp notebook_metadata(notebook) do
|
||||
keys = [:persist_outputs, :autosave_interval_s]
|
||||
keys = [:persist_outputs, :autosave_interval_s, :hub_id]
|
||||
metadata = put_unless_default(%{}, Map.take(notebook, keys), Map.take(Notebook.new(), keys))
|
||||
|
||||
app_settings_metadata = app_settings_metadata(notebook.app_settings)
|
||||
|
@ -245,7 +250,7 @@ defmodule Livebook.LiveMarkdown.Export do
|
|||
|
||||
defp render_metadata(metadata) do
|
||||
metadata_json = Jason.encode!(metadata)
|
||||
"<!-- livebook:#{metadata_json} -->"
|
||||
["<!-- livebook:", metadata_json, " -->"]
|
||||
end
|
||||
|
||||
defp prepend_metadata(iodata, metadata) when metadata == %{}, do: iodata
|
||||
|
@ -312,4 +317,22 @@ defmodule Livebook.LiveMarkdown.Export do
|
|||
|> elem(0)
|
||||
|> Enum.map(fn {_modifiers, string} -> string end)
|
||||
end
|
||||
|
||||
defp render_notebook_footer(notebook, notebook_source) do
|
||||
metadata = notebook_stamp_metadata(notebook)
|
||||
|
||||
with {:ok, hub} <- Livebook.Hubs.get_hub(notebook.hub_id),
|
||||
{:ok, stamp} <- Livebook.Hubs.notebook_stamp(hub, notebook_source, metadata) do
|
||||
offset = IO.iodata_length(notebook_source)
|
||||
json = Jason.encode!(%{"offset" => offset, "stamp" => stamp})
|
||||
["\n", "<!-- livebook:", json, " -->", "\n"]
|
||||
else
|
||||
_ -> []
|
||||
end
|
||||
end
|
||||
|
||||
defp notebook_stamp_metadata(notebook) do
|
||||
keys = [:hub_secret_names]
|
||||
put_unless_default(%{}, Map.take(notebook, keys), Map.take(Notebook.new(), keys))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,10 +8,16 @@ defmodule Livebook.LiveMarkdown.Import do
|
|||
|
||||
{ast, rewrite_messages} = rewrite_ast(ast)
|
||||
elements = group_elements(ast)
|
||||
{stamp_data, elements} = take_stamp_data(elements)
|
||||
{notebook, build_messages} = build_notebook(elements)
|
||||
{notebook, postprocess_messages} = postprocess_notebook(notebook)
|
||||
{notebook, metadata_messages} = postprocess_stamp(notebook, markdown, stamp_data)
|
||||
|
||||
{notebook, earmark_messages ++ rewrite_messages ++ build_messages ++ postprocess_messages}
|
||||
messages =
|
||||
earmark_messages ++
|
||||
rewrite_messages ++ build_messages ++ postprocess_messages ++ metadata_messages
|
||||
|
||||
{notebook, messages}
|
||||
end
|
||||
|
||||
defp earmark_message_to_string({_severity, line_number, message}) do
|
||||
|
@ -171,6 +177,9 @@ defmodule Livebook.LiveMarkdown.Import do
|
|||
%{"livebook_object" => "smart_cell"} ->
|
||||
{:cell, :smart, data}
|
||||
|
||||
%{"stamp" => _} ->
|
||||
{:stamp, data}
|
||||
|
||||
_ ->
|
||||
{:metadata, data}
|
||||
end
|
||||
|
@ -311,7 +320,8 @@ defmodule Livebook.LiveMarkdown.Import do
|
|||
]
|
||||
end
|
||||
|
||||
attrs = 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
|
||||
# other case all extra cells are put in a default section
|
||||
|
@ -361,24 +371,31 @@ defmodule Livebook.LiveMarkdown.Import do
|
|||
defp grab_leading_comments(elems), do: {[], elems}
|
||||
|
||||
defp notebook_metadata_to_attrs(metadata) do
|
||||
Enum.reduce(metadata, %{}, fn
|
||||
{"persist_outputs", persist_outputs}, attrs ->
|
||||
Map.put(attrs, :persist_outputs, persist_outputs)
|
||||
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 ->
|
||||
Map.put(attrs, :autosave_interval_s, autosave_interval_s)
|
||||
{"autosave_interval_s", autosave_interval_s}, {attrs, messages} ->
|
||||
{Map.put(attrs, :autosave_interval_s, autosave_interval_s), messages}
|
||||
|
||||
{"app_settings", app_settings_metadata}, attrs ->
|
||||
{"hub_id", hub_id}, {attrs, messages} ->
|
||||
if Livebook.Hubs.hub_exists?(hub_id) do
|
||||
{Map.put(attrs, :hub_id, hub_id), messages}
|
||||
else
|
||||
{attrs, messages ++ ["ignoring notebook Hub with unknown id"]}
|
||||
end
|
||||
|
||||
{"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)
|
||||
{Map.put(attrs, :app_settings, app_settings), messages}
|
||||
|
||||
_entry, attrs ->
|
||||
attrs
|
||||
_entry, {attrs, messages} ->
|
||||
{attrs, messages}
|
||||
end)
|
||||
end
|
||||
|
||||
|
@ -471,4 +488,40 @@ defmodule Livebook.LiveMarkdown.Import do
|
|||
|
||||
{%{notebook | sections: sections}, Enum.reverse(warnings)}
|
||||
end
|
||||
|
||||
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, []}
|
||||
|
||||
defp postprocess_stamp(notebook, notebook_source, stamp_data) do
|
||||
hub = Livebook.Hubs.fetch_hub!(notebook.hub_id)
|
||||
|
||||
with %{"offset" => offset, "stamp" => stamp} <- stamp_data,
|
||||
{:ok, notebook_source} <- safe_binary_slice(notebook_source, 0, offset),
|
||||
{:ok, metadata} <- Livebook.Hubs.verify_notebook_stamp(hub, notebook_source, stamp) do
|
||||
notebook = apply_stamp_metadata(notebook, metadata)
|
||||
{notebook, []}
|
||||
else
|
||||
_ -> {notebook, ["failed to verify notebook stamp"]}
|
||||
end
|
||||
end
|
||||
|
||||
defp safe_binary_slice(binary, start, size)
|
||||
when byte_size(binary) < start + size,
|
||||
do: :error
|
||||
|
||||
defp safe_binary_slice(binary, start, size) do
|
||||
{:ok, binary_slice(binary, start, size)}
|
||||
end
|
||||
|
||||
defp apply_stamp_metadata(notebook, metadata) do
|
||||
Enum.reduce(metadata, notebook, fn
|
||||
{:hub_secret_names, hub_secret_names}, notebook ->
|
||||
%{notebook | hub_secret_names: hub_secret_names}
|
||||
|
||||
_entry, notebook ->
|
||||
notebook
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
45
lib/livebook/migration.ex
Normal file
45
lib/livebook/migration.ex
Normal file
|
@ -0,0 +1,45 @@
|
|||
defmodule Livebook.Migration do
|
||||
@moduledoc false
|
||||
|
||||
@doc """
|
||||
Runs all migrations.
|
||||
"""
|
||||
@spec migrate() :: :ok
|
||||
def migrate() do
|
||||
insert_personal_hub()
|
||||
move_app_secrets_to_personal_hub()
|
||||
add_personal_hub_secret_key()
|
||||
end
|
||||
|
||||
defp insert_personal_hub() do
|
||||
unless Livebook.Hubs.hub_exists?(Livebook.Hubs.Personal.id()) do
|
||||
Livebook.Hubs.save_hub(%Livebook.Hubs.Personal{
|
||||
id: Livebook.Hubs.Personal.id(),
|
||||
hub_name: "My Hub",
|
||||
hub_emoji: "🏠",
|
||||
secret_key: Livebook.Hubs.Personal.generate_secret_key()
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
defp move_app_secrets_to_personal_hub() do
|
||||
for %{name: name, value: value} <- Livebook.Storage.all(:secrets) do
|
||||
secret = %Livebook.Secrets.Secret{
|
||||
name: name,
|
||||
value: value,
|
||||
hub_id: Livebook.Hubs.Personal.id(),
|
||||
readonly: false
|
||||
}
|
||||
|
||||
Livebook.Secrets.set_secret(secret)
|
||||
Livebook.Storage.delete(:secrets, name)
|
||||
end
|
||||
end
|
||||
|
||||
defp add_personal_hub_secret_key() do
|
||||
with :error <- Livebook.Storage.fetch_key(:hubs, Livebook.Hubs.Personal.id(), :secret_key) do
|
||||
secret_key = Livebook.Hubs.Personal.generate_secret_key()
|
||||
Livebook.Storage.insert(:hubs, Livebook.Hubs.Personal.id(), secret_key: secret_key)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -22,7 +22,9 @@ defmodule Livebook.Notebook do
|
|||
:persist_outputs,
|
||||
:autosave_interval_s,
|
||||
:output_counter,
|
||||
:app_settings
|
||||
:app_settings,
|
||||
:hub_id,
|
||||
:hub_secret_names
|
||||
]
|
||||
|
||||
alias Livebook.Notebook.{Section, Cell, AppSettings}
|
||||
|
@ -38,7 +40,9 @@ defmodule Livebook.Notebook do
|
|||
persist_outputs: boolean(),
|
||||
autosave_interval_s: non_neg_integer() | nil,
|
||||
output_counter: non_neg_integer(),
|
||||
app_settings: AppSettings.t()
|
||||
app_settings: AppSettings.t(),
|
||||
hub_id: String.t(),
|
||||
hub_secret_names: list(String.t())
|
||||
}
|
||||
|
||||
@version "1.0"
|
||||
|
@ -57,7 +61,9 @@ defmodule Livebook.Notebook do
|
|||
persist_outputs: default_persist_outputs(),
|
||||
autosave_interval_s: default_autosave_interval_s(),
|
||||
output_counter: 0,
|
||||
app_settings: AppSettings.new()
|
||||
app_settings: AppSettings.new(),
|
||||
hub_id: Livebook.Hubs.Personal.id(),
|
||||
hub_secret_names: []
|
||||
}
|
||||
|> put_setup_cell(Cell.new(:code))
|
||||
end
|
||||
|
|
|
@ -7,7 +7,7 @@ defmodule Livebook.Secrets.Secret do
|
|||
name: String.t() | nil,
|
||||
value: String.t() | nil,
|
||||
hub_id: String.t() | nil,
|
||||
readonly: boolean()
|
||||
readonly: boolean() | nil
|
||||
}
|
||||
|
||||
@primary_key {:name, :string, autogenerate: false}
|
||||
|
|
|
@ -720,6 +720,7 @@ defmodule Livebook.Session do
|
|||
@impl true
|
||||
def init(opts) do
|
||||
Livebook.Settings.subscribe()
|
||||
Livebook.Hubs.subscribe([:secrets])
|
||||
id = Keyword.fetch!(opts, :id)
|
||||
|
||||
{:ok, worker_pid} = Livebook.Session.Worker.start_link(id)
|
||||
|
@ -1500,6 +1501,13 @@ defmodule Livebook.Session do
|
|||
{:stop, :shutdown, state}
|
||||
end
|
||||
|
||||
def handle_info({event, secret}, state)
|
||||
when event in [:secret_created, :secret_updated, :secret_deleted] and
|
||||
secret.hub_id == state.data.notebook.hub_id do
|
||||
operation = {:sync_hub_secrets, @client_id}
|
||||
{:noreply, handle_operation(state, operation)}
|
||||
end
|
||||
|
||||
def handle_info(_message, state), do: {:noreply, state}
|
||||
|
||||
@impl true
|
||||
|
|
|
@ -32,14 +32,13 @@ defmodule Livebook.Session.Data do
|
|||
:clients_map,
|
||||
:users_map,
|
||||
:secrets,
|
||||
:hub_secrets,
|
||||
:mode,
|
||||
:apps,
|
||||
:app_data,
|
||||
:hub
|
||||
:app_data
|
||||
]
|
||||
|
||||
alias Livebook.{Notebook, Delta, Runtime, JSInterop, FileSystem}
|
||||
alias Livebook.Hubs.Provider
|
||||
alias Livebook.{Notebook, Delta, Runtime, JSInterop, FileSystem, Hubs}
|
||||
alias Livebook.Users.User
|
||||
alias Livebook.Notebook.{Cell, Section, AppSettings}
|
||||
alias Livebook.Utils.Graph
|
||||
|
@ -58,10 +57,10 @@ defmodule Livebook.Session.Data do
|
|||
clients_map: %{client_id() => User.id()},
|
||||
users_map: %{User.id() => User.t()},
|
||||
secrets: %{(name :: String.t()) => value :: String.t()},
|
||||
hub_secrets: list(Livebook.Secrets.Secret.t()),
|
||||
mode: session_mode(),
|
||||
apps: list(app()),
|
||||
app_data: nil | app_data(),
|
||||
hub: Provider.t()
|
||||
app_data: nil | app_data()
|
||||
}
|
||||
|
||||
@type section_info :: %{
|
||||
|
@ -214,6 +213,8 @@ defmodule Livebook.Session.Data do
|
|||
| {:mark_as_not_dirty, client_id()}
|
||||
| {:set_secret, client_id(), secret()}
|
||||
| {:unset_secret, client_id(), String.t()}
|
||||
| {:set_notebook_hub, client_id(), String.t()}
|
||||
| {:sync_hub_secrets, client_id()}
|
||||
| {:set_app_settings, client_id(), AppSettings.t()}
|
||||
| {:add_app, client_id(), Livebook.Session.id(), pid()}
|
||||
| {:set_app_status, client_id(), Livebook.Session.id(), app_status()}
|
||||
|
@ -221,7 +222,6 @@ defmodule Livebook.Session.Data do
|
|||
| {:delete_app, client_id(), Livebook.Session.id()}
|
||||
| {:app_unregistered, client_id()}
|
||||
| {:app_stop, client_id()}
|
||||
| {:set_notebook_hub, client_id(), String.t()}
|
||||
|
||||
@type action ::
|
||||
:connect_runtime
|
||||
|
@ -261,6 +261,15 @@ defmodule Livebook.Session.Data do
|
|||
%{status: :booting, registered: false}
|
||||
end
|
||||
|
||||
hub = Hubs.fetch_hub!(notebook.hub_id)
|
||||
hub_secrets = Hubs.get_secrets(hub)
|
||||
|
||||
secrets =
|
||||
for secret <- hub_secrets,
|
||||
secret.name in notebook.hub_secret_names,
|
||||
do: {secret.name, secret.value},
|
||||
into: %{}
|
||||
|
||||
data = %__MODULE__{
|
||||
notebook: notebook,
|
||||
origin: opts[:origin],
|
||||
|
@ -274,11 +283,11 @@ defmodule Livebook.Session.Data do
|
|||
smart_cell_definitions: [],
|
||||
clients_map: %{},
|
||||
users_map: %{},
|
||||
secrets: %{},
|
||||
secrets: secrets,
|
||||
hub_secrets: hub_secrets,
|
||||
mode: opts[:mode],
|
||||
apps: [],
|
||||
app_data: app_data,
|
||||
hub: Livebook.Hubs.fetch_hub!(Livebook.Hubs.Personal.id())
|
||||
app_data: app_data
|
||||
}
|
||||
|
||||
data
|
||||
|
@ -830,6 +839,7 @@ defmodule Livebook.Session.Data do
|
|||
data
|
||||
|> with_actions()
|
||||
|> set_secret(secret)
|
||||
|> update_notebook_hub_secret_names()
|
||||
|> wrap_ok()
|
||||
end
|
||||
|
||||
|
@ -837,6 +847,29 @@ defmodule Livebook.Session.Data do
|
|||
data
|
||||
|> with_actions()
|
||||
|> unset_secret(secret_name)
|
||||
|> update_notebook_hub_secret_names()
|
||||
|> wrap_ok()
|
||||
end
|
||||
|
||||
def apply_operation(data, {:set_notebook_hub, _client_id, id}) do
|
||||
with {:ok, hub} <- Hubs.get_hub(id) do
|
||||
data
|
||||
|> with_actions()
|
||||
|> set_notebook_hub(hub)
|
||||
|> update_notebook_hub_secret_names()
|
||||
|> set_dirty()
|
||||
|> wrap_ok()
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
def apply_operation(data, {:sync_hub_secrets, _client_id}) do
|
||||
data
|
||||
|> with_actions()
|
||||
|> sync_hub_secrets()
|
||||
|> update_notebook_hub_secret_names()
|
||||
|> set_dirty()
|
||||
|> wrap_ok()
|
||||
end
|
||||
|
||||
|
@ -912,13 +945,6 @@ defmodule Livebook.Session.Data do
|
|||
end
|
||||
end
|
||||
|
||||
def apply_operation(data, {:set_notebook_hub, _client_id, id}) do
|
||||
data
|
||||
|> with_actions()
|
||||
|> set_notebook_hub(id)
|
||||
|> wrap_ok()
|
||||
end
|
||||
|
||||
# ===
|
||||
|
||||
defp with_actions(data, actions \\ []), do: {data, actions}
|
||||
|
@ -1622,6 +1648,27 @@ defmodule Livebook.Session.Data do
|
|||
|> set!(notebook: %{data.notebook | name: name})
|
||||
end
|
||||
|
||||
defp set_notebook_hub({data, _} = data_actions, hub) do
|
||||
data_actions
|
||||
|> set!(
|
||||
notebook: %{data.notebook | hub_id: hub.id},
|
||||
hub_secrets: Hubs.get_secrets(hub)
|
||||
)
|
||||
end
|
||||
|
||||
defp sync_hub_secrets({data, _} = data_actions) do
|
||||
hub = Livebook.Hubs.fetch_hub!(data.notebook.hub_id)
|
||||
secrets = Livebook.Hubs.get_secrets(hub)
|
||||
set!(data_actions, hub_secrets: secrets)
|
||||
end
|
||||
|
||||
defp update_notebook_hub_secret_names({data, _} = data_actions) do
|
||||
hub_secret_names =
|
||||
for secret <- data.hub_secrets, data.secrets[secret.name] == secret.value, do: secret.name
|
||||
|
||||
set!(data_actions, notebook: %{data.notebook | hub_secret_names: hub_secret_names})
|
||||
end
|
||||
|
||||
defp set_section_name({data, _} = data_actions, section, name) do
|
||||
data_actions
|
||||
|> set!(notebook: Notebook.update_section(data.notebook, section.id, &%{&1 | name: name}))
|
||||
|
@ -1835,11 +1882,6 @@ defmodule Livebook.Session.Data do
|
|||
end
|
||||
end
|
||||
|
||||
defp set_notebook_hub(data_actions, id) do
|
||||
hub = Livebook.Hubs.fetch_hub!(id)
|
||||
set!(data_actions, hub: hub)
|
||||
end
|
||||
|
||||
defp set_smart_cell_definitions(data_actions, smart_cell_definitions) do
|
||||
data_actions
|
||||
|> set!(smart_cell_definitions: smart_cell_definitions)
|
||||
|
|
|
@ -8,7 +8,6 @@ defmodule LivebookWeb.SessionLive do
|
|||
alias Livebook.{Sessions, Session, Delta, Notebook, Runtime, LiveMarkdown}
|
||||
alias Livebook.Notebook.{Cell, ContentLoader}
|
||||
alias Livebook.JSInterop
|
||||
alias Livebook.Hubs
|
||||
|
||||
on_mount LivebookWeb.SidebarHook
|
||||
|
||||
|
@ -24,7 +23,6 @@ defmodule LivebookWeb.SessionLive do
|
|||
Session.register_client(session_pid, self(), socket.assigns.current_user)
|
||||
|
||||
Session.subscribe(session_id)
|
||||
Hubs.subscribe(:secrets)
|
||||
Livebook.NotebookManager.subscribe_starred_notebooks()
|
||||
|
||||
{data, client_id}
|
||||
|
@ -60,7 +58,6 @@ defmodule LivebookWeb.SessionLive do
|
|||
data_view: data_to_view(data),
|
||||
autofocus_cell_id: autofocus_cell_id(data.notebook),
|
||||
page_title: get_page_title(data.notebook.name),
|
||||
saved_secrets: Hubs.get_secrets(data.hub),
|
||||
select_secret_ref: nil,
|
||||
select_secret_options: nil,
|
||||
allowed_uri_schemes: Livebook.Config.allowed_uri_schemes(),
|
||||
|
@ -199,8 +196,8 @@ defmodule LivebookWeb.SessionLive do
|
|||
module={LivebookWeb.SessionLive.SecretsListComponent}
|
||||
id="secrets-list"
|
||||
session={@session}
|
||||
saved_secrets={@saved_secrets}
|
||||
hub={@data_view.notebook_hub}
|
||||
saved_secrets={@data_view.hub_secrets}
|
||||
hub={@data_view.hub}
|
||||
secrets={@data_view.secrets}
|
||||
/>
|
||||
</div>
|
||||
|
@ -312,11 +309,11 @@ defmodule LivebookWeb.SessionLive do
|
|||
<:toggle>
|
||||
<div
|
||||
class="inline-flex items-center group cursor-pointer gap-1 mt-1 text-sm text-gray-600 hover:text-gray-800 focus:text-gray-800"
|
||||
aria-label={@data_view.notebook_hub.hub_name}
|
||||
aria-label={@data_view.hub.hub_name}
|
||||
>
|
||||
<span>in</span>
|
||||
<span class="text-lg pl-1"><%= @data_view.notebook_hub.hub_emoji %></span>
|
||||
<span><%= @data_view.notebook_hub.hub_name %></span>
|
||||
<span class="text-lg pl-1"><%= @data_view.hub.hub_emoji %></span>
|
||||
<span><%= @data_view.hub.hub_name %></span>
|
||||
<.remix_icon icon="arrow-down-s-line" class="invisible group-hover:visible" />
|
||||
</div>
|
||||
</:toggle>
|
||||
|
@ -517,8 +514,8 @@ defmodule LivebookWeb.SessionLive do
|
|||
id="secrets"
|
||||
session={@session}
|
||||
secrets={@data_view.secrets}
|
||||
hub={@data_view.notebook_hub}
|
||||
saved_secrets={@saved_secrets}
|
||||
hub={@data_view.hub}
|
||||
saved_secrets={@data_view.hub_secrets}
|
||||
prefill_secret_name={@prefill_secret_name}
|
||||
select_secret_ref={@select_secret_ref}
|
||||
select_secret_options={@select_secret_options}
|
||||
|
@ -1220,33 +1217,6 @@ defmodule LivebookWeb.SessionLive do
|
|||
{:noreply, handle_operation(socket, operation)}
|
||||
end
|
||||
|
||||
def handle_info({:secret_created, _secret}, socket) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> refresh_secrets()
|
||||
|> put_flash(:info, "A new secret has been created on your Livebook Hub")}
|
||||
end
|
||||
|
||||
def handle_info({:secret_updated, _secret}, socket) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> refresh_secrets()
|
||||
|> put_flash(:info, "An existing secret has been updated on your Livebook Hub")}
|
||||
end
|
||||
|
||||
def handle_info({:secret_deleted, _secret}, socket) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> refresh_secrets()
|
||||
|> put_flash(:info, "An existing secret has been deleted on your Livebook Hub")}
|
||||
end
|
||||
|
||||
def handle_info(:hub_changed, socket) do
|
||||
Session.set_notebook_hub(socket.assigns.session.pid, socket.private.data.hub.id)
|
||||
|
||||
{:noreply, refresh_secrets(socket)}
|
||||
end
|
||||
|
||||
def handle_info({:error, error}, socket) do
|
||||
message = error |> to_string() |> upcase_first()
|
||||
|
||||
|
@ -1635,9 +1605,6 @@ defmodule LivebookWeb.SessionLive do
|
|||
prune_cell_sources(socket)
|
||||
end
|
||||
|
||||
defp after_operation(socket, _prev_socket, {:set_notebook_hub, _client_id, _id}),
|
||||
do: refresh_secrets(socket)
|
||||
|
||||
defp after_operation(socket, _prev_socket, _operation), do: socket
|
||||
|
||||
defp handle_actions(socket, actions) do
|
||||
|
@ -1816,10 +1783,11 @@ defmodule LivebookWeb.SessionLive do
|
|||
section_views: section_views(data.notebook.sections, data),
|
||||
bin_entries: data.bin_entries,
|
||||
secrets: data.secrets,
|
||||
hub: Livebook.Hubs.fetch_hub!(data.notebook.hub_id),
|
||||
hub_secrets: data.hub_secrets,
|
||||
apps_status: apps_status(data),
|
||||
app_settings: data.notebook.app_settings,
|
||||
apps: data.apps,
|
||||
notebook_hub: data.hub
|
||||
apps: data.apps
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -2086,7 +2054,4 @@ defmodule LivebookWeb.SessionLive do
|
|||
defp app_status_color(:error), do: "bg-red-400"
|
||||
defp app_status_color(:shutting_down), do: "bg-gray-500"
|
||||
defp app_status_color(:stopped), do: "bg-gray-500"
|
||||
|
||||
defp refresh_secrets(socket),
|
||||
do: assign(socket, saved_secrets: Hubs.get_secrets(socket.private.data.hub))
|
||||
end
|
||||
|
|
2
mix.lock
2
mix.lock
|
@ -27,7 +27,7 @@
|
|||
"phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"},
|
||||
"plug": {:hex, :plug, "1.14.0", "ba4f558468f69cbd9f6b356d25443d0b796fbdc887e03fa89001384a9cac638f", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bf020432c7d4feb7b3af16a0c2701455cbbbb95e5b6866132cb09eb0c29adc14"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.6.0", "d1cf12ff96a1ca4f52207c5271a6c351a4733f413803488d75b70ccf44aebec2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "073cf20b753ce6682ed72905cd62a2d4bd9bad1bf9f7feb02a1b8e525bd94fa6"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"},
|
||||
"protobuf": {:hex, :protobuf, "0.8.0", "61b27d6fd50e7b1b2eb0ee17c1f639906121f4ef965ae0994644eb4c68d4647d", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "3644ed846fd6f5e3b5c2cd617aa8344641e230edf812a45365fee7622bccd25a"},
|
||||
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
|
||||
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
|
||||
|
|
|
@ -35,8 +35,13 @@ defmodule Livebook.Hubs.ProviderTest do
|
|||
end
|
||||
|
||||
test "get_secrets/1 with startup secrets", %{hub: hub} do
|
||||
secret = build(:secret, name: "GET_PERSONAL_SECRET", readonly: true)
|
||||
Livebook.Hubs.Personal.set_startup_secrets([secret])
|
||||
# Set in test_helper.exs
|
||||
secret = %Livebook.Secrets.Secret{
|
||||
name: "STARTUP_SECRET",
|
||||
value: "value",
|
||||
hub_id: Livebook.Hubs.Personal.id(),
|
||||
readonly: true
|
||||
}
|
||||
|
||||
assert secret in Provider.get_secrets(hub)
|
||||
end
|
||||
|
|
|
@ -1121,6 +1121,26 @@ defmodule Livebook.LiveMarkdown.ExportTest do
|
|||
assert expected_document == document
|
||||
end
|
||||
|
||||
test "persists hub id when not default" do
|
||||
Livebook.Factory.insert_hub(:enterprise, id: "enterprise-persisted-id")
|
||||
|
||||
notebook = %{
|
||||
Notebook.new()
|
||||
| name: "My Notebook",
|
||||
hub_id: "enterprise-persisted-id"
|
||||
}
|
||||
|
||||
expected_document = """
|
||||
<!-- livebook:{"hub_id":"enterprise-persisted-id"} -->
|
||||
|
||||
# My Notebook
|
||||
"""
|
||||
|
||||
document = Export.notebook_to_livemd(notebook)
|
||||
|
||||
assert expected_document == document
|
||||
end
|
||||
|
||||
describe "app settings" do
|
||||
test "persists non-default app settings" do
|
||||
notebook = %{
|
||||
|
@ -1190,6 +1210,44 @@ defmodule Livebook.LiveMarkdown.ExportTest do
|
|||
end
|
||||
end
|
||||
|
||||
test "notebook stamp is appended at the end" do
|
||||
notebook = %{
|
||||
Notebook.new()
|
||||
| name: "My Notebook",
|
||||
sections: [
|
||||
%{
|
||||
Notebook.Section.new()
|
||||
| name: "Section 1",
|
||||
cells: [
|
||||
%{
|
||||
Notebook.Cell.new(:code)
|
||||
| source: """
|
||||
IO.puts("hey")
|
||||
"""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
hub_secret_names: ["DB_PASSWORD"]
|
||||
}
|
||||
|
||||
expected_document = ~R"""
|
||||
# My Notebook
|
||||
|
||||
## Section 1
|
||||
|
||||
```elixir
|
||||
IO.puts\("hey"\)
|
||||
```
|
||||
|
||||
<!-- livebook:{"offset":58,"stamp":(.*)} -->
|
||||
"""
|
||||
|
||||
document = Export.notebook_to_livemd(notebook)
|
||||
|
||||
assert document =~ expected_document
|
||||
end
|
||||
|
||||
defp spawn_widget_with_data(ref, data) do
|
||||
spawn(fn ->
|
||||
receive do
|
||||
|
|
|
@ -720,6 +720,33 @@ defmodule Livebook.LiveMarkdown.ImportTest do
|
|||
assert %Notebook{name: "My Notebook", autosave_interval_s: 10} = notebook
|
||||
end
|
||||
|
||||
test "imports notebook hub id when exists" do
|
||||
Livebook.Factory.insert_hub(:fly, id: "enterprise-persisted-id")
|
||||
|
||||
markdown = """
|
||||
<!-- livebook:{"hub_id":"enterprise-persisted-id"} -->
|
||||
|
||||
# My Notebook
|
||||
"""
|
||||
|
||||
{notebook, []} = Import.notebook_from_livemd(markdown)
|
||||
|
||||
assert %Notebook{name: "My Notebook", hub_id: "enterprise-persisted-id"} = notebook
|
||||
end
|
||||
|
||||
test "imports ignores hub id when does not exist" do
|
||||
markdown = """
|
||||
<!-- livebook:{"hub_id":"nonexistent"} -->
|
||||
|
||||
# My Notebook
|
||||
"""
|
||||
|
||||
{notebook, messages} = Import.notebook_from_livemd(markdown)
|
||||
|
||||
assert messages == ["ignoring notebook Hub with unknown id"]
|
||||
assert notebook.hub_id != "nonexistent"
|
||||
end
|
||||
|
||||
describe "app settings" do
|
||||
test "imports settings" do
|
||||
markdown = """
|
||||
|
@ -998,4 +1025,68 @@ defmodule Livebook.LiveMarkdown.ImportTest do
|
|||
} = notebook
|
||||
end
|
||||
end
|
||||
|
||||
describe "notebook stamp" do
|
||||
test "restores hub secret names from notebook stamp" do
|
||||
# Generated with:
|
||||
|
||||
# %{
|
||||
# Notebook.new()
|
||||
# | name: "My Notebook",
|
||||
# sections: [
|
||||
# %{
|
||||
# Notebook.Section.new()
|
||||
# | name: "Section 1",
|
||||
# cells: [
|
||||
# %{
|
||||
# Notebook.Cell.new(:code)
|
||||
# | source: """
|
||||
# IO.puts("hey")
|
||||
# """
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
# ],
|
||||
# hub_secret_names: ["DB_PASSWORD"]
|
||||
# }
|
||||
# |> Livebook.LiveMarkdown.Export.notebook_to_livemd()
|
||||
# |> IO.puts()
|
||||
|
||||
markdown = """
|
||||
# My Notebook
|
||||
|
||||
## Section 1
|
||||
|
||||
```elixir
|
||||
IO.puts("hey")
|
||||
```
|
||||
|
||||
<!-- livebook:{"offset":58,"stamp":{"token":"QTEyOEdDTQ.LF8LTeMYrtq8S7wsKMmk2YgOQzMAkEKT2d8fq1Gz3Ot1mydOgEZ1B4hcEZc.Wec6NwBQ584kE661.a_N-5jDiWrjhHha9zxHQ6JJOmxeqgiya3m6YlKt1Na_DPnEfXyLnengaUzQSrf8.ZoD5r6-H87RpTyvFkvEOQw","version":1}} -->
|
||||
"""
|
||||
|
||||
{notebook, []} = Import.notebook_from_livemd(markdown)
|
||||
|
||||
assert %Notebook{hub_secret_names: ["DB_PASSWORD"]} = notebook
|
||||
end
|
||||
|
||||
test "returns a warning when notebook stamp is invalid" do
|
||||
markdown = """
|
||||
# My Notebook
|
||||
|
||||
## Section 1
|
||||
|
||||
```elixir
|
||||
IO.puts("hey")
|
||||
```
|
||||
|
||||
<!-- livebook:{"offset":58,"stamp":{"token":"invalid","version":1}} -->
|
||||
"""
|
||||
|
||||
{notebook, messages} = Import.notebook_from_livemd(markdown)
|
||||
|
||||
assert %Notebook{hub_secret_names: []} = notebook
|
||||
|
||||
assert messages == ["failed to verify notebook stamp"]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1250,82 +1250,6 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
|
||||
assert output == "\e[32m\"#{secret.value}\"\e[0m"
|
||||
end
|
||||
|
||||
test "loads secret from temporary storage", %{conn: conn, session: session} do
|
||||
secret = build(:secret, name: "FOOBARBAZ", value: "ChonkyCat", readonly: true)
|
||||
Livebook.Hubs.Personal.set_startup_secrets([secret])
|
||||
|
||||
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
|
||||
|
||||
# Subscribe and executes the code to trigger
|
||||
# the `System.EnvError` exception and outputs the 'Add secret' button
|
||||
Session.subscribe(session.id)
|
||||
section_id = insert_section(session.pid)
|
||||
code = ~s{System.fetch_env!("LB_#{secret.name}")}
|
||||
cell_id = insert_text_cell(session.pid, section_id, :code, code)
|
||||
|
||||
# Sets the secret directly
|
||||
Session.set_secret(session.pid, secret)
|
||||
|
||||
# Checks if the secret exists and is inside the session,
|
||||
# then executes the code cell again and checks if the
|
||||
# secret value is what we expected.
|
||||
assert_session_secret(view, session.pid, secret)
|
||||
Session.queue_cell_evaluation(session.pid, cell_id)
|
||||
|
||||
assert_receive {:operation,
|
||||
{:add_cell_evaluation_response, _, ^cell_id, {:text, output}, _}}
|
||||
|
||||
assert output == "\e[32m\"#{secret.value}\"\e[0m"
|
||||
end
|
||||
|
||||
test "granting access for unavailable startup secret using 'Add secret' button",
|
||||
%{conn: conn, session: session, hub: hub} do
|
||||
secret = build(:secret, name: "MYSTARTUPSECRET", value: "ChonkyCat", readonly: true)
|
||||
Livebook.Hubs.Personal.set_startup_secrets([secret])
|
||||
|
||||
# Subscribe and executes the code to trigger
|
||||
# the `System.EnvError` exception and outputs the 'Add secret' button
|
||||
Session.subscribe(session.id)
|
||||
section_id = insert_section(session.pid)
|
||||
code = ~s{System.fetch_env!("LB_#{secret.name}")}
|
||||
cell_id = insert_text_cell(session.pid, section_id, :code, code)
|
||||
|
||||
Session.queue_cell_evaluation(session.pid, cell_id)
|
||||
assert_receive {:operation, {:add_cell_evaluation_response, _, ^cell_id, _, _}}
|
||||
|
||||
# Enters the session to check if the button exists
|
||||
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
|
||||
expected_url = ~p"/sessions/#{session.id}/secrets?secret_name=#{secret.name}"
|
||||
add_secret_button = element(view, "a[href='#{expected_url}']")
|
||||
assert has_element?(add_secret_button)
|
||||
|
||||
# Checks if the secret is persisted
|
||||
assert secret in Livebook.Hubs.get_secrets(hub)
|
||||
|
||||
# Clicks the button and checks if the 'Grant access' banner
|
||||
# is being shown, so clicks it's button to set the app secret
|
||||
# to the session, allowing the user to fetches the secret.
|
||||
render_click(add_secret_button)
|
||||
secrets_component = with_target(view, "#secrets-modal")
|
||||
|
||||
assert render(secrets_component) =~
|
||||
"in #{hub_label(secret)}. Allow this session to access it?"
|
||||
|
||||
grant_access_button = element(secrets_component, "button", "Grant access")
|
||||
render_click(grant_access_button)
|
||||
|
||||
# Checks if the secret exists and is inside the session,
|
||||
# then executes the code cell again and checks if the
|
||||
# secret value is what we expected.
|
||||
assert_session_secret(view, session.pid, secret)
|
||||
Session.queue_cell_evaluation(session.pid, cell_id)
|
||||
|
||||
assert_receive {:operation,
|
||||
{:add_cell_evaluation_response, _, ^cell_id, {:text, output}, _}}
|
||||
|
||||
assert output == "\e[32m\"#{secret.value}\"\e[0m"
|
||||
end
|
||||
end
|
||||
|
||||
describe "environment variables" do
|
||||
|
@ -1489,14 +1413,14 @@ defmodule LivebookWeb.SessionLiveTest do
|
|||
Session.subscribe(session.id)
|
||||
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
|
||||
|
||||
assert %Livebook.Hubs.Personal{id: ^personal_id} = Session.get_data(session.pid).hub
|
||||
assert Session.get_data(session.pid).notebook.hub_id == personal_id
|
||||
|
||||
view
|
||||
|> element(~s/#select-hub-#{id}/)
|
||||
|> render_click()
|
||||
|
||||
assert_receive {:operation, {:set_notebook_hub, _, ^id}}
|
||||
assert Session.get_data(session.pid).hub == hub
|
||||
assert Session.get_data(session.pid).notebook.hub_id == hub.id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -40,6 +40,23 @@ Application.put_env(:livebook, Livebook.Runtime.Embedded,
|
|||
# Disable autosaving
|
||||
Livebook.Storage.insert(:settings, "global", autosave_path: nil)
|
||||
|
||||
# Set a global startup secret, so that there is at least one
|
||||
Livebook.Hubs.Personal.set_startup_secrets([
|
||||
%Livebook.Secrets.Secret{
|
||||
name: "STARTUP_SECRET",
|
||||
value: "value",
|
||||
hub_id: Livebook.Hubs.Personal.id(),
|
||||
readonly: true
|
||||
}
|
||||
])
|
||||
|
||||
# Always use the same secret key in tests
|
||||
secret_key =
|
||||
"5ji8DpnX761QAWXZwSl-2Y-mdW4yTcMimdOJ8SSxCh44wFE0jEbGBUf-VydKwnTLzBiAUedQKs3X_q1j_3lgrw"
|
||||
|
||||
personal_hub = Livebook.Hubs.fetch_hub!(Livebook.Hubs.Personal.id())
|
||||
Livebook.Hubs.Personal.update_hub(personal_hub, %{secret_key: secret_key})
|
||||
|
||||
erl_docs_available? = Code.fetch_docs(:gen_server) != {:error, :chunk_not_found}
|
||||
|
||||
windows? = match?({:win32, _}, :os.type())
|
||||
|
|
Loading…
Add table
Reference in a new issue