Current section

Files

Jump to
livebook lib livebook teams requests.ex
Raw

lib/livebook/teams/requests.ex

defmodule Livebook.Teams.Requests do
alias Livebook.FileSystem
alias Livebook.FileSystems
alias Livebook.Hubs.Team
alias Livebook.Secrets.Secret
alias Livebook.Teams
@org_token_prefix Teams.Constants.org_token_prefix()
@error_message "Something went wrong, try again later or please file a bug if it persists"
@unauthorized_error_message "You are not authorized to perform this action, make sure you have the access and you are not in a Livebook App Server/Offline instance"
@unauthorized_app_deployment_error_message "Deployment not authorized, check deploy permissions for this deployment group"
@typep api_result :: {:ok, map()} | error_result()
@typep error_result :: {:error, map() | String.t()} | {:transport_error, String.t()}
@doc false
def error_message(), do: @error_message
@doc """
Send a request to Livebook Team API to create a new org.
"""
@spec create_org(Teams.Org.t()) :: api_result()
def create_org(org) do
post("/api/v1/org-request", %{name: org.name, key_hash: Teams.Org.key_hash(org)})
end
@doc """
Send a request to Livebook Team API to join an org.
"""
@spec join_org(Teams.Org.t()) :: api_result()
def join_org(org) do
post("/api/v1/org-request/join", %{name: org.name, key_hash: Teams.Org.key_hash(org)})
end
@doc """
Send a request to Livebook Team API to get an org request.
"""
@spec get_org_request_completion_data(pos_integer(), binary) :: api_result()
def get_org_request_completion_data(id, device_code) do
get("/api/v1/org-request/#{id}?device_code=#{device_code}")
end
@doc """
Send a request to Livebook Team API to sign the given payload.
"""
@spec org_sign(Team.t(), String.t()) :: api_result()
def org_sign(team, payload) do
post("/api/v1/org/sign", %{payload: payload}, team)
end
@doc """
Send a request to Livebook Team API to create a secret.
"""
@spec create_secret(Team.t(), Secret.t()) :: api_result()
def create_secret(team, %{deployment_group_id: nil} = secret) do
secret_key = Teams.derive_key(team.teams_key)
secret_value = Teams.encrypt(secret.value, secret_key)
post("/api/v1/org/secrets", %{name: secret.name, value: secret_value}, team)
end
def create_secret(team, secret) do
secret_key = Teams.derive_key(team.teams_key)
secret_value = Teams.encrypt(secret.value, secret_key)
params = %{
name: secret.name,
value: secret_value,
deployment_group_id: secret.deployment_group_id
}
post("/api/v1/org/deployment-groups/secrets", params, team)
end
@doc """
Send a request to Livebook Team API to update a secret.
"""
@spec update_secret(Team.t(), Secret.t()) :: api_result()
def update_secret(team, %{deployment_group_id: nil} = secret) do
secret_key = Teams.derive_key(team.teams_key)
secret_value = Teams.encrypt(secret.value, secret_key)
put("/api/v1/org/secrets", %{name: secret.name, value: secret_value}, team)
end
def update_secret(team, secret) do
secret_key = Teams.derive_key(team.teams_key)
secret_value = Teams.encrypt(secret.value, secret_key)
params = %{
name: secret.name,
value: secret_value,
deployment_group_id: secret.deployment_group_id
}
put("/api/v1/org/deployment-groups/secrets", params, team)
end
@doc """
Send a request to Livebook Team API to delete a secret.
"""
@spec delete_secret(Team.t(), Secret.t()) :: api_result()
def delete_secret(team, %{deployment_group_id: nil} = secret) do
delete("/api/v1/org/secrets", %{name: secret.name}, team)
end
def delete_secret(team, secret) do
params = %{name: secret.name, deployment_group_id: secret.deployment_group_id}
delete("/api/v1/org/deployment-groups/secrets", params, team)
end
@doc """
Send a request to Livebook Team API to create a file system.
"""
@spec create_file_system(Team.t(), FileSystem.t()) :: api_result()
def create_file_system(team, file_system) do
secret_key = Teams.derive_key(team.teams_key)
type = FileSystems.type(file_system)
%{name: name} = FileSystem.external_metadata(file_system)
attrs = FileSystem.dump(file_system)
json = JSON.encode!(attrs)
params = %{
name: name,
type: to_string(type),
value: Teams.encrypt(json, secret_key)
}
post("/api/v1/org/file-systems", params, team)
end
@doc """
Send a request to Livebook Team API to update a file system.
"""
@spec update_file_system(Team.t(), FileSystem.t()) :: api_result()
def update_file_system(team, file_system) do
secret_key = Teams.derive_key(team.teams_key)
type = FileSystems.type(file_system)
%{name: name} = FileSystem.external_metadata(file_system)
attrs = FileSystem.dump(file_system)
json = JSON.encode!(attrs)
params = %{
id: file_system.external_id,
name: name,
type: to_string(type),
value: Teams.encrypt(json, secret_key)
}
put("/api/v1/org/file-systems", params, team)
end
@doc """
Send a request to Livebook Team API to delete a file system.
"""
@spec delete_file_system(Team.t(), FileSystem.t()) :: api_result()
def delete_file_system(team, file_system) do
delete("/api/v1/org/file-systems", %{id: file_system.external_id}, team)
end
@doc """
Send a request to Livebook Team API to create a deployment group.
"""
@spec create_deployment_group(Team.t(), Teams.DeploymentGroup.t()) :: api_result()
def create_deployment_group(team, deployment_group) do
params = %{
name: deployment_group.name,
mode: deployment_group.mode,
clustering: deployment_group.clustering,
url: deployment_group.url
}
post("/api/v1/org/deployment-groups", params, team)
end
@doc """
Send a request to Livebook Team API to deploy an app.
"""
@spec deploy_app(Team.t(), Teams.AppDeployment.t()) :: api_result()
def deploy_app(team, app_deployment) do
secret_key = Teams.derive_key(team.teams_key)
params = %{
title: app_deployment.title,
slug: app_deployment.slug,
multi_session: app_deployment.multi_session,
access_type: app_deployment.access_type,
deployment_group_id: app_deployment.deployment_group_id,
sha: app_deployment.sha
}
encrypted_content = Teams.encrypt(app_deployment.file, secret_key)
upload("/api/v1/org/apps", encrypted_content, params, team)
end
@doc """
Send a request to Livebook Team API to download an app revision.
"""
@spec download_revision(Team.t(), Teams.AppDeployment.t()) :: {:ok, binary()} | error_result()
def download_revision(team, app_deployment) do
params = %{id: app_deployment.id, deployment_group_id: app_deployment.deployment_group_id}
get("/api/v1/org/apps", params, team)
end
@doc """
Send a request to Livebook Team API to create a new auth request.
"""
@spec create_auth_request(Team.t()) :: api_result()
def create_auth_request(team) do
post("/api/v1/org/identity", %{}, team)
end
@doc """
Send a request to Livebook Team API to get the access token from given auth request code.
"""
@spec retrieve_access_token(Team.t(), String.t()) :: api_result()
def retrieve_access_token(team, code) do
post("/api/v1/org/identity/token", %{code: code}, team)
end
@doc """
Send a request to Livebook Team API to get the user information from given access token.
"""
@spec get_user_info(Team.t(), String.t()) :: api_result()
def get_user_info(team, access_token) do
get("/api/v1/org/identity", %{access_token: access_token}, team)
end
@doc """
Send a request to Livebook Team API to return a session using an org token.
"""
@spec fetch_cli_session(map()) :: api_result()
def fetch_cli_session(config) do
post("/api/v1/cli/auth", %{}, config)
end
@doc """
Send a request to Livebook Team API to deploy an app using an org token.
"""
@spec deploy_app_from_cli(Team.t(), Teams.AppDeployment.t(), integer()) :: api_result()
def deploy_app_from_cli(team, app_deployment, deployment_group_id) do
secret_key = Teams.derive_key(team.teams_key)
params = %{
title: app_deployment.title,
slug: app_deployment.slug,
multi_session: app_deployment.multi_session,
access_type: app_deployment.access_type,
deployment_group_id: deployment_group_id,
sha: app_deployment.sha
}
encrypted_content = Teams.encrypt(app_deployment.file, secret_key)
upload("/api/v1/cli/org/apps", encrypted_content, params, team)
end
@doc """
Normalizes errors map into errors for the given schema.
"""
@spec to_error_list(module(), %{String.t() => list(String.t())}) ::
list({atom(), list(String.t())})
def to_error_list(struct, errors_map) do
fields = struct.__schema__(:fields) |> MapSet.new()
for {key, errors} <- errors_map,
field = String.to_atom(key),
field in fields,
do: {field, errors}
end
defp post(path, json, team \\ nil) do
build_req(team)
|> Req.post(url: path, json: json)
|> handle_response()
|> dispatch_messages(team)
end
defp put(path, json, team) do
build_req(team)
|> Req.put(url: path, json: json)
|> handle_response()
|> dispatch_messages(team)
end
defp delete(path, json, team) do
build_req(team)
|> Req.delete(url: path, json: json)
|> handle_response()
|> dispatch_messages(team)
end
defp get(path, params \\ %{}, team \\ nil) do
build_req(team)
|> Req.get(url: path, params: params)
|> handle_response()
end
defp upload(path, content, params, team) do
build_req(team)
|> Req.Request.put_header("content-length", "#{byte_size(content)}")
|> Req.Request.append_response_steps(
livebook_put_private: fn {request, response} ->
{request, Req.Response.put_private(response, :livebook_app_deployment, true)}
end
)
|> Req.post(url: path, params: params, body: content)
|> handle_response()
|> dispatch_messages(team)
end
defp build_req(team) do
Req.new(base_url: Livebook.Config.teams_url())
|> Req.Request.put_new_header("x-lb-version", Livebook.Config.app_version())
|> Livebook.Utils.req_attach_defaults()
|> add_team_auth(team)
end
defp add_team_auth(req, nil), do: req
defp add_team_auth(req, %{offline: %{}}) do
Req.Request.append_request_steps(req, unauthorized: &{&1, Req.Response.new(status: 401)})
end
defp add_team_auth(req, %{session_token: @org_token_prefix <> _} = team) do
token = "#{team.session_token}:#{Teams.Org.key_hash(%Teams.Org{teams_key: team.teams_key})}"
Req.Request.merge_options(req, auth: {:bearer, token})
end
defp add_team_auth(req, %{user_id: nil} = team) do
agent_name = Livebook.Config.agent_name()
token = "#{team.session_token}:#{agent_name}:#{team.org_id}:#{team.org_key_id}"
Req.Request.merge_options(req, auth: {:bearer, token})
end
defp add_team_auth(req, team) do
token = "#{team.user_id}:#{team.org_id}:#{team.org_key_id}:#{team.session_token}"
Req.Request.merge_options(req, auth: {:bearer, token})
end
defp handle_response(response) do
case response do
{:ok, %{status: status} = response} when status in 200..299 ->
{:ok, response.body}
{:ok, %{status: status} = response} when status in [403, 410, 422] ->
return_error(response)
{:ok, %{status: 401, private: %{livebook_app_deployment: true}}} ->
{:transport_error, @unauthorized_app_deployment_error_message}
{:ok, %{status: 401}} ->
{:transport_error, @unauthorized_error_message}
_otherwise ->
{:transport_error, @error_message}
end
end
defp dispatch_messages({:ok, %{"messages" => _} = body}, %Livebook.Hubs.Team{} = team) do
{messages, body} = Map.pop!(body, "messages")
for message <- messages do
%{type: event} =
message
|> Base.url_decode64!(padding: false)
|> LivebookProto.Event.decode()
Livebook.Hubs.TeamClient.handle_event(team.id, event)
end
{:ok, body}
end
defp dispatch_messages(result, _), do: result
defp return_error(response) do
if json?(response),
do: {:error, response.body},
else: {:transport_error, response.body}
end
defp json?(response) do
"application/json; charset=utf-8" in Req.Response.get_header(response, "content-type")
end
end