Packages
livebook
0.19.4
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/zta/livebook_teams.ex
defmodule Livebook.ZTA.LivebookTeams do
use LivebookWeb, :verified_routes
alias Livebook.Teams
import Plug.Conn
import Phoenix.Controller
@behaviour NimbleZTA
@impl true
def child_spec(opts) do
%{id: __MODULE__, start: {__MODULE__, :start_link, [opts]}}
end
def start_link(opts) do
name = Keyword.fetch!(opts, :name)
identity_key = Keyword.fetch!(opts, :identity_key)
team = Livebook.Hubs.fetch_hub!(identity_key)
NimbleZTA.put(name, team)
:ignore
end
@impl true
def authenticate(name, conn, _opts) do
team = NimbleZTA.get(name)
if Livebook.Hubs.TeamClient.identity_enabled?(team.id) do
handle_request(conn, team, conn.params)
else
{conn, %{}}
end
end
# Our extension to NimbleZTA to deal with logouts
def logout(name, conn) do
token = get_session(conn, :livebook_teams_access_token)
team = NimbleZTA.get(name)
url =
Livebook.Config.teams_url()
|> URI.new!()
|> URI.append_path("/identity/logout")
|> URI.append_query("org_id=#{team.org_id}&access_token=#{token}")
|> URI.to_string()
conn
|> configure_session(renew: true)
|> clear_session()
|> redirect(external: url)
end
defp handle_request(conn, team, %{"teams_identity" => _, "code" => code}) do
with {:ok, access_token} <- retrieve_access_token(team, code),
{:ok, metadata} <- get_user_info(team, access_token) do
{conn
|> put_session(:livebook_teams_access_token, access_token)
|> redirect(to: conn.request_path)
|> halt(), metadata}
else
_ ->
{conn
|> put_session(:teams_error, true)
|> redirect(to: conn.request_path)
|> halt(), nil}
end
end
defp handle_request(conn, _team, %{"teams_identity" => _, "failed_reason" => reason}) do
{conn
|> put_session(:teams_failed_reason, reason)
|> redirect(to: conn.request_path)
|> halt(), nil}
end
defp handle_request(conn, team, %{"teams_redirect" => _, "redirect_to" => redirect_to}) do
case Teams.Requests.create_auth_request(team) do
{:ok, %{"authorize_uri" => authorize_uri}} ->
uri =
authorize_uri
|> URI.new!()
|> URI.append_query("redirect_to=#{redirect_to}")
{conn
|> redirect(external: URI.to_string(uri))
|> halt(), nil}
{_error_or_transport_error, _reason} ->
{conn
|> put_session(:teams_error, true)
|> redirect(to: conn.request_path)
|> halt(), nil}
end
end
defp handle_request(conn, team, _params) do
case get_session(conn) do
%{"livebook_teams_access_token" => access_token} ->
validate_access_token(conn, team, access_token)
# it means, we couldn't reach to Teams server
%{"teams_error" => true} ->
{conn
|> put_status(:bad_request)
|> delete_session(:teams_error)
|> put_view(LivebookWeb.ErrorHTML)
|> render("400.html", %{status: 400})
|> halt(), nil}
%{"teams_failed_reason" => reason} ->
{conn
|> put_status(:forbidden)
|> delete_session(:teams_failed_reason)
|> put_view(LivebookWeb.ErrorHTML)
|> render("error.html", %{
status: 403,
details: "Failed to authenticate with Livebook Teams: #{reason}"
})
|> halt(), nil}
_ ->
request_user_authentication(conn)
end
end
defp validate_access_token(conn, team, access_token) do
case get_user_info(team, access_token) do
{:ok, metadata} -> {conn, metadata}
_ -> request_user_authentication(conn)
end
end
defp retrieve_access_token(team, code) do
with {:ok, %{"access_token" => access_token}} <-
Teams.Requests.retrieve_access_token(team, code) do
{:ok, access_token}
end
end
defp request_user_authentication(conn) do
# We have the browser do the redirect because the browser
# knows the current page location. Unfortunately, it is quite
# complex to know the actual host on the server, because the
# user may be running inside a proxy. So in order to make the
# feature more accessible, we do the redirecting on the client.
html_document = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Redirecting...</title>
<script>
const redirectTo = new URL(window.location.href);
redirectTo.searchParams.append("teams_identity", "");
const url = new URL(window.location.href);
url.searchParams.set("redirect_to", redirectTo.toString());
url.searchParams.append("teams_redirect", "");
window.location.href = url.toString();
</script>
</head>
</html>
"""
{conn |> html(html_document) |> halt(), nil}
end
defp get_user_info(team, access_token) do
with {:ok, payload} <- Teams.Requests.get_user_info(team, access_token) do
{:ok, build_metadata(team.id, payload)}
end
end
@doc """
Returns the user metadata from given payload.
"""
@spec build_metadata(String.t(), map()) :: NimbleZTA.metadata()
def build_metadata(hub_id, payload) do
%{
"id" => id,
"name" => name,
"email" => email,
"groups" => groups,
"avatar_url" => avatar_url
} = payload
access_type =
if Livebook.Hubs.TeamClient.user_full_access?(hub_id, groups),
do: :full,
else: :apps
%{
id: id,
name: name,
avatar_url: avatar_url,
access_type: access_type,
groups: groups,
email: email,
payload: payload
}
end
end