Packages
livebook
0.11.0
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/team.ex
defmodule Livebook.Hubs.Team do
use Ecto.Schema
import Ecto.Changeset
defmodule Offline do
use Ecto.Schema
@type t :: %__MODULE__{
file_systems: list(Livebook.FileSystem.t()),
secrets: list(Livebook.Secrets.Secret.t())
}
@primary_key false
embedded_schema do
field :secrets, {:array, :map}, default: []
field :file_systems, {:array, :map}, default: []
end
end
@type t :: %__MODULE__{
id: String.t() | nil,
org_id: non_neg_integer() | nil,
user_id: non_neg_integer() | nil,
org_key_id: non_neg_integer() | nil,
teams_key: String.t() | nil,
org_public_key: String.t() | nil,
session_token: String.t() | nil,
hub_name: 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
field :org_key_id, :integer
field :teams_key, :string
field :org_public_key, :string
field :session_token, :string
field :hub_name, :string
field :hub_emoji, :string
embeds_one :offline, Offline
end
@fields ~w(
org_id
user_id
org_key_id
teams_key
org_public_key
session_token
hub_name
hub_emoji
)a
@doc """
Initializes a new Team hub.
"""
@spec new() :: t()
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.
"""
@spec change_hub(t(), map()) :: Ecto.Changeset.t()
def change_hub(%__MODULE__{} = team, attrs \\ %{}) do
changeset(team, attrs)
end
defp changeset(team, attrs) do
team
|> cast(attrs, @fields)
|> validate_required(@fields)
|> add_id()
end
defp add_id(changeset) do
if name = get_field(changeset, :hub_name) do
change(changeset, %{id: "team-#{name}"})
else
changeset
end
end
@doc """
Returns the public key prefix
"""
@spec public_key_prefix() :: String.t()
def public_key_prefix(), do: "lb_opk_"
end
defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Team do
alias Livebook.Hubs.TeamClient
alias Livebook.Teams
@teams_key_prefix Teams.Org.teams_key_prefix()
@public_key_prefix Livebook.Hubs.Team.public_key_prefix()
def load(team, fields) do
struct(team, fields)
end
def to_metadata(team) do
%Livebook.Hubs.Metadata{
id: team.id,
name: team.hub_name,
provider: team,
emoji: team.hub_emoji,
connected?: TeamClient.connected?(team.id)
}
end
def type(_team), do: "team"
def connection_spec(team), do: {TeamClient, team}
def disconnect(team), do: TeamClient.stop(team.id)
def get_secrets(team), do: TeamClient.get_secrets(team.id)
def create_secret(team, secret), do: Teams.create_secret(team, secret)
def update_secret(team, secret), do: Teams.update_secret(team, secret)
def delete_secret(team, secret), do: Teams.delete_secret(team, secret)
def connection_error(team) do
if reason = TeamClient.get_connection_error(team.id) do
"Cannot connect to Hub: #{reason}.\nWill attempt to reconnect automatically..."
end
end
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
# then validate the signature using the corresponding public key.
#
# This results in a two factor mechanism, where creating a valid
# stamp requires access to the shared local key and an authenticated
# request to the Teams server (which ensures team membership).
@teams_key_prefix <> teams_key = team.teams_key
token = Livebook.Stamping.chapoly_encrypt(metadata, notebook_source, teams_key)
case Livebook.Teams.org_sign(team, token) do
{:ok, token_signature} ->
stamp = %{"version" => 1, "token" => token, "token_signature" => token_signature}
{:ok, stamp}
_ ->
{:error, "request to Livebook Teams failed"}
end
end
def verify_notebook_stamp(team, notebook_source, stamp) do
%{"version" => 1, "token" => token, "token_signature" => token_signature} = stamp
@teams_key_prefix <> teams_key = team.teams_key
@public_key_prefix <> org_public_key = team.org_public_key
if Livebook.Stamping.rsa_verify?(token_signature, token, org_public_key) do
Livebook.Stamping.chapoly_decrypt(token, notebook_source, teams_key)
else
{:error, :invalid}
end
end
def dump(team) do
team
|> Map.from_struct()
|> Map.delete(:offline)
end
def get_file_systems(team) do
TeamClient.get_file_systems(team.id)
end
def create_file_system(team, file_system), do: Teams.create_file_system(team, file_system)
def update_file_system(team, file_system), do: Teams.update_file_system(team, file_system)
def delete_file_system(team, file_system), do: Teams.delete_file_system(team, file_system)
end