Packages
livebook
0.19.1
0.19.8
0.19.7
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.6
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.3
0.17.2
0.17.1
0.17.0
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.14.0-rc.1
0.14.0-rc.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.2.0
0.1.2
0.1.1
0.1.0
Automate code & data workflows with interactive notebooks
Current section
Files
Jump to
Current section
Files
lib/livebook/hubs/personal.ex
defmodule Livebook.Hubs.Personal do
use Ecto.Schema
import Ecto.Changeset
alias Livebook.FileSystem
alias Livebook.FileSystems
alias Livebook.Hubs
alias Livebook.Storage
alias Livebook.Secrets.Secret
@secrets_namespace :hub_secrets
@secret_key_size 64
@file_systems_namespace :file_systems
@type t :: %__MODULE__{
id: String.t() | nil,
hub_name: 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 secret_key)a
@doc """
The personal hub fixed id.
"""
@spec id() :: String.t()
def id, do: "personal-hub"
@doc """
Returns an `%Ecto.Changeset{}` for tracking hub changes.
"""
@spec change_hub(t(), map()) :: Ecto.Changeset.t()
def change_hub(%__MODULE__{} = personal, attrs \\ %{}) do
changeset(personal, attrs)
end
@doc """
Returns changeset with applied validations.
"""
@spec validate_hub(t(), map()) :: Ecto.Changeset.t()
def validate_hub(%__MODULE__{} = personal, attrs \\ %{}) do
personal
|> changeset(attrs)
|> Map.put(:action, :validate)
end
@doc """
Updates a Hub.
With success, notifies interested processes about hub metadata data change.
Otherwise, it will return an error tuple with changeset.
"""
@spec update_hub(t(), map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
def update_hub(%__MODULE__{} = personal, attrs) do
changeset = changeset(personal, attrs)
with {:ok, struct} <- apply_action(changeset, :update) do
Hubs.save_hub(struct)
{:ok, struct}
end
end
defp changeset(personal, attrs) do
personal
|> cast(attrs, @fields)
|> validate_required(@fields)
|> validate_change(:secret_key, fn :secret_key, secret_key ->
case Base.url_decode64(secret_key, padding: false) do
{:ok, binary} when byte_size(binary) == @secret_key_size -> []
_ -> [secret_key: "must be #{@secret_key_size} bytes in Base 64 URL alphabet"]
end
end)
|> put_change(:id, id())
end
@doc """
Get the secrets list from storage.
"""
@spec get_secrets() :: list(Secret.t())
def get_secrets() do
Enum.map(Storage.all(@secrets_namespace), &to_secret/1)
end
@doc """
Gets a secret from storage.
Raises `RuntimeError` if the secret doesn't exist.
"""
@spec fetch_secret!(String.t()) :: Secret.t()
def fetch_secret!(id) do
Storage.fetch!(@secrets_namespace, id) |> to_secret()
end
@doc """
Stores the given secret as is, without validation.
"""
@spec set_secret(Secret.t()) :: Secret.t()
def set_secret(secret) do
attributes = Map.from_struct(secret)
:ok = Storage.insert(@secrets_namespace, secret.name, Map.to_list(attributes))
secret
end
@doc """
Unset secret from given id.
"""
@spec unset_secret(String.t()) :: :ok
def unset_secret(id) do
Storage.delete(@secrets_namespace, id)
:ok
end
defp to_secret(%{name: name, value: value}) do
%Secret{
name: name,
value: value,
hub_id: Livebook.Hubs.Personal.id()
}
end
@doc """
Generates a random secret key used for stamping the notebook.
"""
@spec generate_secret_key() :: String.t()
def generate_secret_key() do
Base.url_encode64(:crypto.strong_rand_bytes(@secret_key_size), padding: false)
end
@doc """
Get the file systems list from storage.
"""
@spec get_file_systems() :: list(FileSystem.t())
def get_file_systems() do
Storage.all(@file_systems_namespace)
|> Enum.map(&to_file_system/1)
|> Enum.sort_by(&FileSystem.external_metadata(&1).name)
end
@doc """
Gets a file system from storage.
Raises `RuntimeError` if the file system does not exist.
"""
@spec fetch_file_system!(String.t()) :: FileSystem.t()
def fetch_file_system!(id) do
Storage.fetch!(@file_systems_namespace, id) |> to_file_system()
end
@doc """
Saves a new file system to the configured ones.
"""
@spec save_file_system(FileSystem.t()) :: FileSystem.t()
def save_file_system(file_system) do
attributes = FileSystem.dump(file_system)
type = FileSystems.type(file_system)
storage_attributes = Map.put(attributes, :type, type)
:ok = Storage.insert(@file_systems_namespace, file_system.id, Map.to_list(storage_attributes))
file_system
end
@doc """
Removes the given file system from the configured ones.
"""
@spec remove_file_system(FileSystem.id()) :: :ok
def remove_file_system(id) do
Storage.delete(@file_systems_namespace, id)
end
defp to_file_system(fields) do
FileSystems.load(fields.type, fields)
end
end
defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Personal do
alias Livebook.Hubs.Broadcasts
alias Livebook.Hubs.Personal
def load(personal, fields) do
%{
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
%Livebook.Hubs.Metadata{
id: personal.id,
name: personal.hub_name,
provider: personal,
emoji: personal.hub_emoji,
connected?: false
}
end
def type(_personal), do: "personal"
def connection_spec(_personal), do: nil
def disconnect(_personal), do: raise("not implemented")
def get_secrets(_personal) do
Personal.get_secrets()
end
def create_secret(_personal, secret) do
Personal.set_secret(secret)
:ok = Broadcasts.secret_created(secret)
end
def update_secret(_personal, secret) do
Personal.set_secret(secret)
:ok = Broadcasts.secret_updated(secret)
end
def delete_secret(_personal, secret) do
:ok = Personal.unset_secret(secret.name)
:ok = Broadcasts.secret_deleted(secret)
end
def connection_status(_personal), do: raise("not implemented")
def notebook_stamp(_hub, _notebook_source, metadata) when metadata == %{} do
:skip
end
def notebook_stamp(personal, notebook_source, metadata) do
token = Livebook.Stamping.chapoly_encrypt(metadata, notebook_source, personal.secret_key)
stamp = %{"version" => 2, "token" => token}
{:ok, stamp}
end
def verify_notebook_stamp(personal, notebook_source, stamp) do
case stamp do
%{"version" => 1, "token" => token} ->
Livebook.Stamping.aead_decrypt(token, notebook_source, personal.secret_key)
%{"version" => 2, "token" => token} ->
Livebook.Stamping.chapoly_decrypt(token, notebook_source, personal.secret_key)
%{"version" => _} ->
{:error, :too_recent_version}
end
end
def dump(personal) do
Map.from_struct(personal)
end
def get_file_systems(_personal) do
Personal.get_file_systems()
end
def create_file_system(_personal, file_system) do
Personal.save_file_system(file_system)
:ok = Broadcasts.file_system_created(file_system)
end
def update_file_system(_personal, file_system) do
Personal.save_file_system(file_system)
:ok = Broadcasts.file_system_updated(file_system)
end
def delete_file_system(_personal, file_system) do
:ok = Personal.remove_file_system(file_system.id)
:ok = Broadcasts.file_system_deleted(file_system)
end
def deployment_groups(_personal), do: nil
def get_app_specs(_personal), do: []
def get_app_folders(_personal), do: []
end