Handle authorization groups in real-time (#2998)

This commit is contained in:
Alexandre de Souza 2025-05-12 10:43:03 -03:00 committed by GitHub
parent 7fa44a3966
commit 73c0f1b45c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 950 additions and 91 deletions

View file

@ -7,6 +7,7 @@ defmodule Livebook.Apps do
require Logger require Logger
alias Livebook.App alias Livebook.App
alias Livebook.Apps
@doc """ @doc """
Returns app process pid for the given slug. Returns app process pid for the given slug.
@ -79,13 +80,10 @@ defmodule Livebook.Apps do
@spec authorized?(App.t(), Livebook.Users.User.t()) :: boolean() @spec authorized?(App.t(), Livebook.Users.User.t()) :: boolean()
def authorized?(app, user) def authorized?(app, user)
def authorized?(%{app_spec: %Livebook.Apps.TeamsAppSpec{}}, %{restricted_apps_groups: []}), def authorized?(_app, %{access_type: :full}), do: true
do: false
def authorized?(_app, %{restricted_apps_groups: nil}), do: true def authorized?(%{slug: slug, app_spec: %Apps.TeamsAppSpec{hub_id: id}}, user) do
Livebook.Hubs.TeamClient.user_app_access?(id, user.groups, slug)
def authorized?(%{slug: slug, app_spec: %Livebook.Apps.TeamsAppSpec{hub_id: id}}, user) do
Livebook.Hubs.TeamClient.user_app_access?(id, user.restricted_apps_groups, slug)
end end
@doc """ @doc """

View file

@ -296,27 +296,40 @@ defmodule Livebook.Hubs.TeamClient do
end end
end end
def handle_call({:check_full_access, groups}, _caller, %{deployment_group_id: id} = state) do def handle_call({:check_full_access, groups}, _caller, state) do
if id = state.deployment_group_id do
case fetch_deployment_group(id, state) do case fetch_deployment_group(id, state) do
{:ok, deployment_group} -> {:ok, deployment_group} ->
{:reply, authorized_group?(deployment_group.authorization_groups, groups), state} {:reply,
not deployment_group.teams_auth or
not deployment_group.groups_auth or
authorized_group?(deployment_group.authorization_groups, groups), state}
_ -> _ ->
{:reply, false, state} {:reply, false, state}
end end
else
{:reply, true, state}
end
end end
def handle_call({:check_app_access, groups, slug}, _caller, %{deployment_group_id: id} = state) do def handle_call({:check_app_access, groups, slug}, _caller, state) do
if id = state.deployment_group_id do
with {:ok, deployment_group} <- fetch_deployment_group(id, state), with {:ok, deployment_group} <- fetch_deployment_group(id, state),
{:ok, app_deployment} <- fetch_app_deployment_from_slug(slug, state) do {:ok, app_deployment} <- fetch_app_deployment_from_slug(slug, state) do
app_access? = app_access? =
authorized_group?(deployment_group.authorization_groups, groups) or not deployment_group.teams_auth or
authorized_group?(app_deployment.authorization_groups, groups) not deployment_group.groups_auth or
(authorized_group?(deployment_group.authorization_groups, groups) or
authorized_group?(app_deployment.authorization_groups, groups))
{:reply, app_access?, state} {:reply, app_access?, state}
else else
_ -> {:reply, false, state} _ -> {:reply, false, state}
end end
else
{:reply, true, state}
end
end end
@impl true @impl true
@ -492,6 +505,7 @@ defmodule Livebook.Hubs.TeamClient do
clustering: nullify(deployment_group.clustering), clustering: nullify(deployment_group.clustering),
url: nullify(deployment_group.url), url: nullify(deployment_group.url),
teams_auth: deployment_group.teams_auth, teams_auth: deployment_group.teams_auth,
groups_auth: deployment_group.groups_auth,
authorization_groups: authorization_groups authorization_groups: authorization_groups
} }
end end
@ -531,6 +545,7 @@ defmodule Livebook.Hubs.TeamClient do
clustering: atomize(deployment_group_updated.clustering), clustering: atomize(deployment_group_updated.clustering),
url: nullify(deployment_group_updated.url), url: nullify(deployment_group_updated.url),
teams_auth: deployment_group_updated.teams_auth, teams_auth: deployment_group_updated.teams_auth,
groups_auth: deployment_group_updated.groups_auth,
authorization_groups: authorization_groups authorization_groups: authorization_groups
} }
end end
@ -659,7 +674,6 @@ defmodule Livebook.Hubs.TeamClient do
defp handle_event(:deployment_group_created, %Teams.DeploymentGroup{} = deployment_group, state) do defp handle_event(:deployment_group_created, %Teams.DeploymentGroup{} = deployment_group, state) do
Teams.Broadcasts.deployment_group_created(deployment_group) Teams.Broadcasts.deployment_group_created(deployment_group)
put_deployment_group(state, deployment_group) put_deployment_group(state, deployment_group)
end end
@ -673,6 +687,16 @@ defmodule Livebook.Hubs.TeamClient do
defp handle_event(:deployment_group_updated, %Teams.DeploymentGroup{} = deployment_group, state) do defp handle_event(:deployment_group_updated, %Teams.DeploymentGroup{} = deployment_group, state) do
Teams.Broadcasts.deployment_group_updated(deployment_group) Teams.Broadcasts.deployment_group_updated(deployment_group)
with {:ok, current_deployment_group} <- fetch_deployment_group(deployment_group.id, state) do
if state.deployment_group_id == deployment_group.id and
(current_deployment_group.authorization_groups != deployment_group.authorization_groups or
current_deployment_group.groups_auth != deployment_group.groups_auth or
current_deployment_group.teams_auth != deployment_group.teams_auth) do
Teams.Broadcasts.server_authorization_updated(deployment_group)
end
end
put_deployment_group(state, deployment_group) put_deployment_group(state, deployment_group)
end end

View file

@ -7,6 +7,7 @@ defmodule Livebook.Teams.Broadcasts do
@app_deployments_topic "teams:app_deployments" @app_deployments_topic "teams:app_deployments"
@clients_topic "teams:clients" @clients_topic "teams:clients"
@deployment_groups_topic "teams:deployment_groups" @deployment_groups_topic "teams:deployment_groups"
@app_server_topic "teams:app_server"
@doc """ @doc """
Subscribes to one or more subtopics in `"teams"`. Subscribes to one or more subtopics in `"teams"`.
@ -31,9 +32,13 @@ defmodule Livebook.Teams.Broadcasts do
Topic `#{@deployment_groups_topic}`: Topic `#{@deployment_groups_topic}`:
* `{:deployment_group_created, DeploymentGroup.t()}` * `{:deployment_group_created, DeploymentGroup.t()}`
* `{:deployment_group_update, DeploymentGroup.t()}` * `{:deployment_group_updated, DeploymentGroup.t()}`
* `{:deployment_group_deleted, DeploymentGroup.t()}` * `{:deployment_group_deleted, DeploymentGroup.t()}`
Topic `#{@app_server_topic}`:
* `{:server_authorization_updated, DeploymentGroup.t()}`
""" """
@spec subscribe(atom() | list(atom())) :: :ok | {:error, term()} @spec subscribe(atom() | list(atom())) :: :ok | {:error, term()}
def subscribe(topics) when is_list(topics) do def subscribe(topics) when is_list(topics) do
@ -132,6 +137,14 @@ defmodule Livebook.Teams.Broadcasts do
broadcast(@agents_topic, {:agent_left, agent}) broadcast(@agents_topic, {:agent_left, agent})
end end
@doc """
Broadcasts under `#{@app_server_topic}` topic when hub received a updated deployment group that changed which groups have access to the server.
"""
@spec server_authorization_updated(Teams.DeploymentGroup.t()) :: broadcast()
def server_authorization_updated(%Teams.DeploymentGroup{} = deployment_group) do
broadcast(@app_server_topic, {:server_authorization_updated, deployment_group})
end
defp broadcast(topic, message) do defp broadcast(topic, message) do
Phoenix.PubSub.broadcast(Livebook.PubSub, topic, message) Phoenix.PubSub.broadcast(Livebook.PubSub, topic, message)
end end

View file

@ -13,6 +13,7 @@ defmodule Livebook.Teams.DeploymentGroup do
clustering: :auto | :dns | nil, clustering: :auto | :dns | nil,
hub_id: String.t() | nil, hub_id: String.t() | nil,
teams_auth: boolean(), teams_auth: boolean(),
groups_auth: boolean(),
authorization_groups: Ecto.Schema.embeds_many(Teams.AuthorizationGroup.t()), authorization_groups: Ecto.Schema.embeds_many(Teams.AuthorizationGroup.t()),
secrets: Ecto.Schema.has_many(Secrets.Secret.t()), secrets: Ecto.Schema.has_many(Secrets.Secret.t()),
agent_keys: Ecto.Schema.has_many(Teams.AgentKey.t()), agent_keys: Ecto.Schema.has_many(Teams.AgentKey.t()),
@ -27,6 +28,7 @@ defmodule Livebook.Teams.DeploymentGroup do
field :clustering, Ecto.Enum, values: [:auto, :dns] field :clustering, Ecto.Enum, values: [:auto, :dns]
field :url, :string field :url, :string
field :teams_auth, :boolean, default: true field :teams_auth, :boolean, default: true
field :groups_auth, :boolean, default: false
has_many :secrets, Secrets.Secret has_many :secrets, Secrets.Secret
has_many :agent_keys, Teams.AgentKey has_many :agent_keys, Teams.AgentKey
@ -36,7 +38,7 @@ defmodule Livebook.Teams.DeploymentGroup do
def changeset(deployment_group, attrs \\ %{}) do def changeset(deployment_group, attrs \\ %{}) do
deployment_group deployment_group
|> cast(attrs, [:id, :name, :mode, :hub_id, :clustering, :url, :teams_auth]) |> cast(attrs, [:id, :name, :mode, :hub_id, :clustering, :url])
|> validate_required([:name, :mode]) |> validate_required([:name, :mode])
|> update_change(:url, fn url -> |> update_change(:url, fn url ->
if url do if url do

View file

@ -172,8 +172,7 @@ defmodule Livebook.Teams.Requests do
name: deployment_group.name, name: deployment_group.name,
mode: deployment_group.mode, mode: deployment_group.mode,
clustering: deployment_group.clustering, clustering: deployment_group.clustering,
url: deployment_group.url, url: deployment_group.url
teams_auth: deployment_group.teams_auth
} }
post("/api/v1/org/deployment-groups", params, team) post("/api/v1/org/deployment-groups", params, team)

View file

@ -12,12 +12,15 @@ defmodule Livebook.Users.User do
alias Livebook.Utils alias Livebook.Utils
@type access_type :: :full | :apps
@type t :: %__MODULE__{ @type t :: %__MODULE__{
id: id(), id: id(),
name: String.t() | nil, name: String.t() | nil,
email: String.t() | nil, email: String.t() | nil,
avatar_url: String.t() | nil, avatar_url: String.t() | nil,
restricted_apps_groups: list(map()) | nil, access_type: access_type(),
groups: list(map()) | nil,
payload: map() | nil, payload: map() | nil,
hex_color: hex_color() hex_color: hex_color()
} }
@ -29,7 +32,8 @@ defmodule Livebook.Users.User do
field :name, :string field :name, :string
field :email, :string field :email, :string
field :avatar_url, :string field :avatar_url, :string
field :restricted_apps_groups, {:array, :map} field :access_type, Ecto.Enum, values: ~w[full apps]a, default: :full
field :groups, {:array, :map}, default: []
field :payload, :map field :payload, :map
field :hex_color, Livebook.EctoTypes.HexColor field :hex_color, Livebook.EctoTypes.HexColor
end end
@ -44,15 +48,17 @@ defmodule Livebook.Users.User do
name: nil, name: nil,
email: nil, email: nil,
avatar_url: nil, avatar_url: nil,
restricted_apps_groups: nil, access_type: :full,
groups: [],
payload: nil, payload: nil,
hex_color: Livebook.EctoTypes.HexColor.random() hex_color: Livebook.EctoTypes.HexColor.random()
} }
end end
@doc false
def changeset(user, attrs \\ %{}) do def changeset(user, attrs \\ %{}) do
user user
|> cast(attrs, [:name, :email, :avatar_url, :restricted_apps_groups, :hex_color, :payload]) |> cast(attrs, [:name, :email, :avatar_url, :access_type, :groups, :hex_color, :payload])
|> validate_required([:hex_color]) |> validate_required([:hex_color])
end end
end end

View file

@ -58,6 +58,9 @@ defmodule Livebook.ZTA do
* `:id` - a string that uniquely identifies the user * `:id` - a string that uniquely identifies the user
* `:name` - the user name * `:name` - the user name
* `:email` - the user email * `:email` - the user email
* `:avatar_url` - the user avatar
* `:access_type` - the user access type
* `:groups` - the user groups
* `:payload` - the provider payload * `:payload` - the provider payload
Note that none of the keys are required. The metadata returned depends Note that none of the keys are required. The metadata returned depends
@ -67,6 +70,9 @@ defmodule Livebook.ZTA do
optional(:id) => String.t(), optional(:id) => String.t(),
optional(:name) => String.t(), optional(:name) => String.t(),
optional(:email) => String.t(), optional(:email) => String.t(),
optional(:avatar_url) => String.t() | nil,
optional(:access_type) => Livebook.Users.User.access_type(),
optional(:groups) => list(map()),
optional(:payload) => map() optional(:payload) => map()
} }

View file

@ -159,6 +159,15 @@ defmodule Livebook.ZTA.LivebookTeams do
defp get_user_info(team, access_token) do defp get_user_info(team, access_token) do
with {:ok, payload} <- Teams.Requests.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()) :: Livebook.ZTA.metadata()
def build_metadata(hub_id, payload) do
%{ %{
"id" => id, "id" => id,
"name" => name, "name" => name,
@ -167,23 +176,19 @@ defmodule Livebook.ZTA.LivebookTeams do
"avatar_url" => avatar_url "avatar_url" => avatar_url
} = payload } = payload
restricted_apps_groups = access_type =
if Livebook.Hubs.TeamClient.user_full_access?(team.id, groups) do if Livebook.Hubs.TeamClient.user_full_access?(hub_id, groups),
nil do: :full,
else else: :apps
groups
end
metadata = %{ %{
id: id, id: id,
name: name, name: name,
avatar_url: avatar_url, avatar_url: avatar_url,
restricted_apps_groups: restricted_apps_groups, access_type: access_type,
groups: groups,
email: email, email: email,
payload: payload payload: payload
} }
{:ok, metadata}
end
end end
end end

View file

@ -12,8 +12,13 @@ defmodule LivebookWeb.AppLive do
end end
end end
def mount(_params, _session, socket) when not socket.assigns.app_authorized? do def mount(%{"slug" => slug}, _session, socket) when not socket.assigns.app_authorized? do
{:ok, socket, layout: false} if connected?(socket) do
Livebook.Teams.Broadcasts.subscribe(:app_deployments)
end
{:ok, app} = Livebook.Apps.fetch_app(slug)
{:ok, assign(socket, app: app), layout: false}
end end
def mount(%{"slug" => slug}, _session, socket) do def mount(%{"slug" => slug}, _session, socket) do
@ -22,6 +27,7 @@ defmodule LivebookWeb.AppLive do
if connected?(socket) do if connected?(socket) do
Livebook.App.subscribe(slug) Livebook.App.subscribe(slug)
Livebook.Teams.Broadcasts.subscribe(:app_deployments)
end end
{:ok, assign(socket, app: app)} {:ok, assign(socket, app: app)}
@ -122,6 +128,18 @@ defmodule LivebookWeb.AppLive do
{:noreply, assign(socket, :app, app)} {:noreply, assign(socket, :app, app)}
end end
def handle_info(
{:app_deployment_updated, %{slug: slug}},
%{assigns: %{app: %{slug: slug} = app}} = socket
) do
if socket.assigns.app_authorized? and
Livebook.Apps.authorized?(app, socket.assigns.current_user) do
{:noreply, socket}
else
{:noreply, redirect(socket, to: ~p"/apps/#{slug}")}
end
end
def handle_info(_message, socket), do: {:noreply, socket} def handle_info(_message, socket), do: {:noreply, socket}
defp active_sessions(sessions) do defp active_sessions(sessions) do

View file

@ -24,8 +24,13 @@ defmodule LivebookWeb.AppSessionLive do
end end
end end
def mount(_params, _session, socket) when not socket.assigns.app_authorized? do def mount(%{"slug" => slug, "id" => session_id}, _session, socket)
{:ok, socket, layout: false} when not socket.assigns.app_authorized? do
if connected?(socket) do
Livebook.Teams.Broadcasts.subscribe(:app_deployments)
end
{:ok, assign(socket, slug: slug, session: %{id: session_id}), layout: false}
end end
def mount(%{"slug" => slug, "id" => session_id}, _session, socket) do def mount(%{"slug" => slug, "id" => session_id}, _session, socket) do
@ -388,7 +393,8 @@ defmodule LivebookWeb.AppSessionLive do
# should't have access. # should't have access.
{:ok, app} = Livebook.Apps.fetch_app(slug) {:ok, app} = Livebook.Apps.fetch_app(slug)
if Livebook.Apps.authorized?(app, socket.assigns.current_user) do if socket.assigns.app_authorized? and
Livebook.Apps.authorized?(app, socket.assigns.current_user) do
{:noreply, socket} {:noreply, socket}
else else
{:noreply, redirect(socket, to: ~p"/apps/#{slug}/sessions/#{socket.assigns.session.id}")} {:noreply, redirect(socket, to: ~p"/apps/#{slug}/sessions/#{socket.assigns.session.id}")}

View file

@ -4,7 +4,7 @@ defmodule LivebookWeb.AppsLive do
@impl true @impl true
def mount(_params, _session, socket) do def mount(_params, _session, socket) do
if connected?(socket) do if connected?(socket) do
Livebook.Teams.Broadcasts.subscribe(:app_deployments) Livebook.Teams.Broadcasts.subscribe(:app_server)
Livebook.Apps.subscribe() Livebook.Apps.subscribe()
end end
@ -101,7 +101,7 @@ defmodule LivebookWeb.AppsLive do
{:noreply, update(socket, :apps, &LivebookWeb.AppComponents.update_app_list(&1, event))} {:noreply, update(socket, :apps, &LivebookWeb.AppComponents.update_app_list(&1, event))}
end end
def handle_info({:app_deployment_updated, _app_deployment}, socket) do def handle_info({:server_authorization_updated, _}, socket) do
apps = Livebook.Apps.list_authorized_apps(socket.assigns.current_user) apps = Livebook.Apps.list_authorized_apps(socket.assigns.current_user)
{:noreply, assign(socket, :apps, apps)} {:noreply, assign(socket, :apps, apps)}
end end

View file

@ -5,6 +5,7 @@ defmodule LivebookWeb.AuthHook do
def on_mount(:default, _params, session, socket) do def on_mount(:default, _params, session, socket) do
uri = get_connect_info(socket, :uri) uri = get_connect_info(socket, :uri)
socket = attach_hook(socket, :authorization_subscription, :handle_info, &handle_info/2)
if LivebookWeb.AuthPlug.authorized?(session || %{}, uri.port) do if LivebookWeb.AuthPlug.authorized?(session || %{}, uri.port) do
{:cont, socket} {:cont, socket}
@ -12,4 +13,19 @@ defmodule LivebookWeb.AuthHook do
{:halt, redirect(socket, to: ~p"/")} {:halt, redirect(socket, to: ~p"/")}
end end
end end
defp handle_info({:server_authorization_updated, %{hub_id: hub_id}}, socket) do
# We already updated the current user, so we just need to force the redirection.
# But, for apps, we redirect directly from the app session
current_user = socket.assigns.current_user
if current_user.access_type == :full and
Livebook.Hubs.TeamClient.user_full_access?(hub_id, current_user.groups) do
{:halt, socket}
else
{:halt, redirect(socket, to: ~p"/")}
end
end
defp handle_info(_message, socket), do: {:cont, socket}
end end

View file

@ -1,4 +1,6 @@
defmodule LivebookWeb.UserHook do defmodule LivebookWeb.UserHook do
use LivebookWeb, :verified_routes
import Phoenix.Component import Phoenix.Component
import Phoenix.LiveView import Phoenix.LiveView
@ -13,10 +15,12 @@ defmodule LivebookWeb.UserHook do
user_data = connect_params["user_data"] || session["user_data"] user_data = connect_params["user_data"] || session["user_data"]
LivebookWeb.UserPlug.build_current_user(session, identity_data, user_data) LivebookWeb.UserPlug.build_current_user(session, identity_data, user_data)
end) end)
|> attach_hook(:current_user_subscription, :handle_info, &info/2) |> attach_hook(:current_user_subscription, :handle_info, &handle_info/2)
|> attach_hook(:server_authorization_subscription, :handle_info, &handle_info/2)
if connected?(socket) do if connected?(socket) do
Livebook.Users.subscribe(socket.assigns.current_user.id) Livebook.Users.subscribe(socket.assigns.current_user.id)
Livebook.Teams.Broadcasts.subscribe(:app_server)
end end
Logger.metadata(Livebook.Utils.logger_users_metadata([socket.assigns.current_user])) Logger.metadata(Livebook.Utils.logger_users_metadata([socket.assigns.current_user]))
@ -24,12 +28,25 @@ defmodule LivebookWeb.UserHook do
{:cont, socket} {:cont, socket}
end end
defp info( defp handle_info(
{:user_change, %{id: id} = user}, {:user_change, %{id: id} = user},
%{assigns: %{current_user: %{id: id}}} = socket %{assigns: %{current_user: %{id: id}}} = socket
) do ) do
{:halt, assign(socket, :current_user, user)} {:halt, assign(socket, :current_user, user)}
end end
defp info(_message, socket), do: {:cont, socket} defp handle_info({:server_authorization_updated, deployment_group}, socket) do
# Since we checks if the updated deployment group we received belongs
# to the current app server, we don't need to check here.
current_user = socket.assigns.current_user
hub_id = deployment_group.hub_id
metadata = Livebook.ZTA.LivebookTeams.build_metadata(hub_id, current_user.payload)
case Livebook.Users.update_user(current_user, metadata) do
{:ok, user} -> {:cont, assign(socket, :current_user, user)}
_otherwise -> {:cont, socket}
end
end
defp handle_info(_message, socket), do: {:cont, socket}
end end

View file

@ -64,7 +64,7 @@ defmodule LivebookWeb.AuthPlug do
get_session(conn), get_session(conn),
conn.assigns.identity_data, conn.assigns.identity_data,
conn.assigns.user_data conn.assigns.user_data
).restricted_apps_groups == nil ).access_type == :full
end end
@doc """ @doc """
@ -77,7 +77,7 @@ defmodule LivebookWeb.AuthPlug do
session, session,
session["identity_data"], session["identity_data"],
session["user_data"] session["user_data"]
).restricted_apps_groups == nil ).access_type == :full
end end
defp authenticate(conn) do defp authenticate(conn) do
@ -136,6 +136,7 @@ defmodule LivebookWeb.AuthPlug do
conn conn
|> put_status(:unauthorized) |> put_status(:unauthorized)
|> put_view(LivebookWeb.ErrorHTML) |> put_view(LivebookWeb.ErrorHTML)
|> put_root_layout(false)
|> render("401.html", %{details: "You don't have permission to access this server"}) |> render("401.html", %{details: "You don't have permission to access this server"})
|> halt() |> halt()
end end

View file

@ -34,7 +34,7 @@ defmodule LivebookWeb.UserPlug do
end end
defp ensure_user_identity(conn) do defp ensure_user_identity(conn) do
{_type, module, _key} = Livebook.Config.identity_provider() {_type, module, _key} = identity_provider(conn)
{conn, identity_data} = module.authenticate(LivebookWeb.ZTA, conn, []) {conn, identity_data} = module.authenticate(LivebookWeb.ZTA, conn, [])
cond do cond do
@ -128,4 +128,22 @@ defmodule LivebookWeb.UserPlug do
"user_data" => conn.assigns.user_data "user_data" => conn.assigns.user_data
} }
end end
@doc """
Returns the identity provider configuration for the given `conn` or
`session`.
This mirrors `Livebook.Config.identity_provider/0`, except the it can
be overridden in tests, for each connection.
"""
@spec identity_provider(Plug.Conn.t() | map()) :: {atom(), module, binary}
if Mix.env() == :test do
def identity_provider(%Plug.Conn{} = conn), do: identity_provider(get_session(conn))
def identity_provider(%{} = session) do
session["identity_provider_test_override"] || Livebook.Config.identity_provider()
end
else
def identity_provider(_), do: Livebook.Config.identity_provider()
end
end end

View file

@ -22,4 +22,6 @@ defmodule LivebookProto.DeploymentGroup do
repeated: true, repeated: true,
type: LivebookProto.AuthorizationGroup, type: LivebookProto.AuthorizationGroup,
json_name: "authorizationGroups" json_name: "authorizationGroups"
field :groups_auth, 13, type: :bool, json_name: "groupsAuth"
end end

View file

@ -21,4 +21,6 @@ defmodule LivebookProto.DeploymentGroupUpdated do
repeated: true, repeated: true,
type: LivebookProto.AuthorizationGroup, type: LivebookProto.AuthorizationGroup,
json_name: "authorizationGroups" json_name: "authorizationGroups"
field :groups_auth, 12, type: :bool, json_name: "groupsAuth"
end end

View file

@ -67,6 +67,7 @@ message DeploymentGroup {
repeated EnvironmentVariable environment_variables = 10; repeated EnvironmentVariable environment_variables = 10;
bool teams_auth = 11; bool teams_auth = 11;
repeated AuthorizationGroup authorization_groups = 12; repeated AuthorizationGroup authorization_groups = 12;
bool groups_auth = 13;
} }
message DeploymentGroupCreated { message DeploymentGroupCreated {
@ -93,6 +94,7 @@ message DeploymentGroupUpdated {
repeated EnvironmentVariable environment_variables = 9; repeated EnvironmentVariable environment_variables = 9;
bool teams_auth = 10; bool teams_auth = 10;
repeated AuthorizationGroup authorization_groups = 11; repeated AuthorizationGroup authorization_groups = 11;
bool groups_auth = 12;
} }
message DeploymentGroupDeleted { message DeploymentGroupDeleted {

View file

@ -4,6 +4,7 @@ defmodule LivebookWeb.Integration.AdminLiveTest do
import Phoenix.LiveViewTest import Phoenix.LiveViewTest
describe "topbar" do
setup %{teams_auth: teams_auth} do setup %{teams_auth: teams_auth} do
Application.put_env(:livebook, :teams_auth, teams_auth) Application.put_env(:livebook, :teams_auth, teams_auth)
on_exit(fn -> Application.delete_env(:livebook, :teams_auth) end) on_exit(fn -> Application.delete_env(:livebook, :teams_auth) end)
@ -28,4 +29,182 @@ defmodule LivebookWeb.Integration.AdminLiveTest do
"You are running an offline Workspace for deployment. You cannot modify its settings." "You are running an offline Workspace for deployment. You cannot modify its settings."
end end
end end
end
describe "authorization" do
setup %{conn: conn, node: node} do
Livebook.Teams.Broadcasts.subscribe([:agents, :app_server])
Livebook.Apps.subscribe()
{_agent_key, org, deployment_group, team} = create_agent_team_hub(node)
# we wait until the agent_connected is received by livebook
hub_id = team.id
deployment_group_id = to_string(deployment_group.id)
org_id = to_string(org.id)
assert_receive {:agent_joined,
%{
hub_id: ^hub_id,
org_id: ^org_id,
deployment_group_id: ^deployment_group_id
}}
start_supervised!(
{Livebook.ZTA.LivebookTeams, name: LivebookWeb.ZTA, identity_key: team.id}
)
{conn, code} = authenticate_user_on_teams(conn, node, team)
{:ok, conn: conn, code: code, deployment_group: deployment_group, org: org, team: team}
end
test "renders unauthorized admin page if user doesn't have full access",
%{conn: conn, node: node, code: code} = context do
erpc_call(node, :toggle_groups_authorization, [context.deployment_group])
oidc_provider = erpc_call(node, :create_oidc_provider, [context.org])
authorization_group =
erpc_call(node, :create_authorization_group, [
%{
group_name: "marketing",
access_type: :apps,
prefixes: ["dev-"],
oidc_provider: oidc_provider,
deployment_group: context.deployment_group
}
])
erpc_call(node, :update_user_info_groups, [
code,
[
%{
"provider_id" => to_string(oidc_provider.id),
"group_name" => authorization_group.group_name
}
]
])
assert conn
|> get(~p"/settings")
|> html_response(401) =~ "Not authorized"
end
test "shows admin page if user have full access",
%{conn: conn, node: node, code: code} = context do
erpc_call(node, :toggle_groups_authorization, [context.deployment_group])
oidc_provider = erpc_call(node, :create_oidc_provider, [context.org])
authorization_group =
erpc_call(node, :create_authorization_group, [
%{
group_name: "marketing",
access_type: :app_server,
oidc_provider: oidc_provider,
deployment_group: context.deployment_group
}
])
erpc_call(node, :update_user_info_groups, [
code,
[
%{
"provider_id" => to_string(oidc_provider.id),
"group_name" => authorization_group.group_name
}
]
])
{:ok, _view, html} = live(conn, ~p"/settings")
assert html =~ "System settings"
end
test "renders unauthorized if loses the access in real-time",
%{conn: conn, node: node, code: code} = context do
{:ok, deployment_group} =
erpc_call(node, :toggle_groups_authorization, [context.deployment_group])
oidc_provider = erpc_call(node, :create_oidc_provider, [context.org])
authorization_group =
erpc_call(node, :create_authorization_group, [
%{
group_name: "marketing",
access_type: :app_server,
oidc_provider: oidc_provider,
deployment_group: deployment_group
}
])
erpc_call(node, :update_user_info_groups, [
code,
[
%{
"provider_id" => to_string(oidc_provider.id),
"group_name" => authorization_group.group_name
}
]
])
{:ok, view, _html} = live(conn, ~p"/settings")
assert render(view) =~ "System settings"
erpc_call(node, :update_authorization_group, [
authorization_group,
%{access_type: :apps, prefixes: ["ops-"]}
])
id = to_string(deployment_group.id)
assert_receive {:server_authorization_updated, %{id: ^id}}
# If you lose access to the app server, we will redirect to "/"
assert_redirect view, ~p"/"
# And it will redirect to "/apps"
{:ok, view, _html} = live(conn, ~p"/apps")
assert render(view) =~ "No apps running."
end
test "shows admin page if authentication is disabled",
%{conn: conn, node: node, code: code} = context do
{:ok, deployment_group} =
erpc_call(node, :toggle_groups_authorization, [context.deployment_group])
oidc_provider = erpc_call(node, :create_oidc_provider, [context.org])
authorization_group =
erpc_call(node, :create_authorization_group, [
%{
group_name: "marketing",
access_type: :apps,
prefixes: ["ops-"],
oidc_provider: oidc_provider,
deployment_group: deployment_group
}
])
erpc_call(node, :update_user_info_groups, [
code,
[
%{
"provider_id" => to_string(oidc_provider.id),
"group_name" => authorization_group.group_name
}
]
])
assert conn
|> get(~p"/settings")
|> html_response(401) =~ "Not authorized"
{:ok, %{teams_auth: false} = deployment_group} =
erpc_call(node, :toggle_teams_authentication, [deployment_group])
id = to_string(deployment_group.id)
assert_receive {:server_authorization_updated, %{id: ^id, teams_auth: false}}
{:ok, view, _html} = live(conn, ~p"/settings")
assert render(view) =~ "System settings"
end
end
end end

View file

@ -0,0 +1,253 @@
defmodule LivebookWeb.Integration.AppSessionLiveTest do
use Livebook.TeamsIntegrationCase, async: false
import Phoenix.LiveViewTest
describe "authorized apps" do
setup %{conn: conn, node: node} do
Livebook.Teams.Broadcasts.subscribe([:agents, :app_deployments, :app_server])
Livebook.Apps.subscribe()
{_agent_key, org, deployment_group, team} = create_agent_team_hub(node)
# we wait until the agent_connected is received by livebook
hub_id = team.id
deployment_group_id = to_string(deployment_group.id)
org_id = to_string(org.id)
assert_receive {:agent_joined,
%{
hub_id: ^hub_id,
org_id: ^org_id,
deployment_group_id: ^deployment_group_id
}}
start_supervised!(
{Livebook.ZTA.LivebookTeams, name: LivebookWeb.ZTA, identity_key: team.id}
)
{conn, code} = authenticate_user_on_teams(conn, node, team)
{:ok, conn: conn, code: code, deployment_group: deployment_group, org: org, team: team}
end
@tag :tmp_dir
test "shows app if user doesn't have full access",
%{conn: conn, node: node, code: code, tmp_dir: tmp_dir} = context do
erpc_call(node, :toggle_groups_authorization, [context.deployment_group])
oidc_provider = erpc_call(node, :create_oidc_provider, [context.org])
authorization_group =
erpc_call(node, :create_authorization_group, [
%{
group_name: "marketing",
access_type: :apps,
prefixes: ["dev-"],
oidc_provider: oidc_provider,
deployment_group: context.deployment_group
}
])
erpc_call(node, :update_user_info_groups, [
code,
[
%{
"provider_id" => to_string(oidc_provider.id),
"group_name" => authorization_group.group_name
}
]
])
slug = "dev-app"
pid = deploy_app(slug, context.team, context.org, context.deployment_group, tmp_dir, node)
session_id = Livebook.App.get_session_id(pid, user: Livebook.Users.User.new())
{:ok, _view, html} = live(conn, ~p"/apps/#{slug}/sessions/#{session_id}")
assert html =~ "LivebookApp:#{slug}"
end
@tag :tmp_dir
test "shows app if user have full access",
%{conn: conn, node: node, code: code, tmp_dir: tmp_dir} = context do
erpc_call(node, :toggle_groups_authorization, [context.deployment_group])
oidc_provider = erpc_call(node, :create_oidc_provider, [context.org])
authorization_group =
erpc_call(node, :create_authorization_group, [
%{
group_name: "marketing",
access_type: :app_server,
oidc_provider: oidc_provider,
deployment_group: context.deployment_group
}
])
erpc_call(node, :update_user_info_groups, [
code,
[
%{
"provider_id" => to_string(oidc_provider.id),
"group_name" => authorization_group.group_name
}
]
])
slugs = ~w(mkt-app sales-app opt-app)
for slug <- slugs do
pid = deploy_app(slug, context.team, context.org, context.deployment_group, tmp_dir, node)
session_id = Livebook.App.get_session_id(pid, user: Livebook.Users.User.new())
{:ok, _view, html} = live(conn, ~p"/apps/#{slug}/sessions/#{session_id}")
assert html =~ "LivebookApp:#{slug}"
end
end
@tag :tmp_dir
test "renders unauthorized if loses the access in real-time",
%{conn: conn, node: node, code: code, tmp_dir: tmp_dir} = context do
{:ok, deployment_group} =
erpc_call(node, :toggle_groups_authorization, [context.deployment_group])
oidc_provider = erpc_call(node, :create_oidc_provider, [context.org])
authorization_group =
erpc_call(node, :create_authorization_group, [
%{
group_name: "marketing",
access_type: :apps,
prefixes: ["mkt-"],
oidc_provider: oidc_provider,
deployment_group: deployment_group
}
])
erpc_call(node, :update_user_info_groups, [
code,
[
%{
"provider_id" => to_string(oidc_provider.id),
"group_name" => authorization_group.group_name
}
]
])
slug = "mkt-analytics"
pid = deploy_app(slug, context.team, context.org, context.deployment_group, tmp_dir, node)
session_id = Livebook.App.get_session_id(pid, user: Livebook.Users.User.new())
path = ~p"/apps/#{slug}/sessions/#{session_id}"
{:ok, view, _html} = live(conn, path)
assert render(view) =~ "LivebookApp:#{slug}"
erpc_call(node, :update_authorization_group, [authorization_group, %{prefixes: ["ops-"]}])
id = to_string(deployment_group.id)
assert_receive {:server_authorization_updated, %{id: ^id}}
assert_receive {:app_deployment_updated, %{slug: ^slug}}
assert_redirect view, path
{:ok, view, _html} = live(conn, path)
assert render(view) =~ "Not authorized"
end
@tag :tmp_dir
test "shows app if disable the authentication in real-time",
%{conn: conn, node: node, code: code, tmp_dir: tmp_dir} = context do
{:ok, deployment_group} =
erpc_call(node, :toggle_groups_authorization, [context.deployment_group])
oidc_provider = erpc_call(node, :create_oidc_provider, [context.org])
authorization_group =
erpc_call(node, :create_authorization_group, [
%{
group_name: "marketing",
access_type: :apps,
prefixes: ["mkt-"],
oidc_provider: oidc_provider,
deployment_group: deployment_group
}
])
erpc_call(node, :update_user_info_groups, [
code,
[
%{
"provider_id" => to_string(oidc_provider.id),
"group_name" => authorization_group.group_name
}
]
])
slug = "analytics-app"
pid = deploy_app(slug, context.team, context.org, context.deployment_group, tmp_dir, node)
session_id = Livebook.App.get_session_id(pid, user: Livebook.Users.User.new())
path = ~p"/apps/#{slug}/sessions/#{session_id}"
{:ok, view, _html} = live(conn, path)
assert render(view) =~ "Not authorized"
{:ok, %{teams_auth: false} = deployment_group} =
erpc_call(node, :toggle_teams_authentication, [deployment_group])
id = to_string(deployment_group.id)
assert_receive {:server_authorization_updated, %{id: ^id, teams_auth: false}}
assert_redirect view, path
{:ok, view, _html} = live(conn, path)
assert render(view) =~ "LivebookApp:#{slug}"
end
end
defp deploy_app(slug, team, org, deployment_group, tmp_dir, node) do
source = """
<!-- livebook:{"app_settings":{"access_type":"public","slug":"#{slug}"},"hub_id":"#{team.id}","deployment_group_id":"#{deployment_group.id}"} -->
# LivebookApp:#{slug}
```elixir
```
"""
{notebook, %{warnings: []}} = Livebook.LiveMarkdown.notebook_from_livemd(source)
files_dir = Livebook.FileSystem.File.local(tmp_dir)
{:ok, %Livebook.Teams.AppDeployment{file: zip_content} = app_deployment} =
Livebook.Teams.AppDeployment.new(notebook, files_dir)
secret_key = Livebook.Teams.derive_key(team.teams_key)
encrypted_content = Livebook.Teams.encrypt(zip_content, secret_key)
app_deployment_id =
erpc_call(node, :upload_app_deployment, [
org,
deployment_group,
app_deployment,
encrypted_content,
# broadcast?
true
]).id
app_deployment_id = to_string(app_deployment_id)
assert_receive {:app_deployment_started, %{id: ^app_deployment_id}}
assert_receive {:app_created, %{pid: pid, slug: ^slug}}
assert_receive {:app_updated,
%{
slug: ^slug,
sessions: [%{app_status: %{execution: :executed, lifecycle: :active}}]
}}
on_exit(fn ->
if Process.alive?(pid) do
Livebook.App.close(pid)
end
end)
pid
end
end

View file

@ -0,0 +1,260 @@
defmodule LivebookWeb.Integration.AppsLiveTest do
use Livebook.TeamsIntegrationCase, async: false
describe "authorized apps" do
setup %{conn: conn, node: node} do
Livebook.Teams.Broadcasts.subscribe([:agents, :app_deployments, :app_server])
Livebook.Apps.subscribe()
{_agent_key, org, deployment_group, team} = create_agent_team_hub(node)
# we wait until the agent_connected is received by livebook
hub_id = team.id
deployment_group_id = to_string(deployment_group.id)
org_id = to_string(org.id)
assert_receive {:agent_joined,
%{
hub_id: ^hub_id,
org_id: ^org_id,
deployment_group_id: ^deployment_group_id
}}
start_supervised!(
{Livebook.ZTA.LivebookTeams, name: LivebookWeb.ZTA, identity_key: team.id}
)
{conn, code} = authenticate_user_on_teams(conn, node, team)
{:ok, conn: conn, code: code, deployment_group: deployment_group, org: org, team: team}
end
@tag :tmp_dir
test "shows one app if user doesn't have full access",
%{conn: conn, node: node, code: code, tmp_dir: tmp_dir} = context do
erpc_call(node, :toggle_groups_authorization, [context.deployment_group])
oidc_provider = erpc_call(node, :create_oidc_provider, [context.org])
authorization_group =
erpc_call(node, :create_authorization_group, [
%{
group_name: "marketing",
access_type: :apps,
prefixes: ["dev-"],
oidc_provider: oidc_provider,
deployment_group: context.deployment_group
}
])
erpc_call(node, :update_user_info_groups, [
code,
[
%{
"provider_id" => to_string(oidc_provider.id),
"group_name" => authorization_group.group_name
}
]
])
slug = "dev-app"
deploy_app(slug, context.team, context.org, context.deployment_group, tmp_dir, node)
html =
conn
|> get(~p"/apps")
|> html_response(200)
refute html =~ "No apps running."
assert html =~ slug
end
@tag :tmp_dir
test "shows all apps if user have full access",
%{conn: conn, node: node, code: code, tmp_dir: tmp_dir} = context do
erpc_call(node, :toggle_groups_authorization, [context.deployment_group])
oidc_provider = erpc_call(node, :create_oidc_provider, [context.org])
authorization_group =
erpc_call(node, :create_authorization_group, [
%{
group_name: "marketing",
access_type: :app_server,
oidc_provider: oidc_provider,
deployment_group: context.deployment_group
}
])
erpc_call(node, :update_user_info_groups, [
code,
[
%{
"provider_id" => to_string(oidc_provider.id),
"group_name" => authorization_group.group_name
}
]
])
slugs = ~w(mkt-app sales-app opt-app)
for slug <- slugs do
deploy_app(slug, context.team, context.org, context.deployment_group, tmp_dir, node)
end
html =
conn
|> get(~p"/apps")
|> html_response(200)
refute html =~ "No apps running."
for slug <- slugs do
assert html =~ slug
end
end
@tag :tmp_dir
test "updates the apps list in real-time",
%{conn: conn, node: node, code: code, tmp_dir: tmp_dir} = context do
{:ok, deployment_group} =
erpc_call(node, :toggle_groups_authorization, [context.deployment_group])
oidc_provider = erpc_call(node, :create_oidc_provider, [context.org])
authorization_group =
erpc_call(node, :create_authorization_group, [
%{
group_name: "marketing",
access_type: :apps,
prefixes: ["mkt-"],
oidc_provider: oidc_provider,
deployment_group: deployment_group
}
])
erpc_call(node, :update_user_info_groups, [
code,
[
%{
"provider_id" => to_string(oidc_provider.id),
"group_name" => authorization_group.group_name
}
]
])
slug = "marketing-app"
deploy_app(slug, context.team, context.org, context.deployment_group, tmp_dir, node)
assert conn
|> get(~p"/apps")
|> html_response(200) =~ "No apps running."
{:ok, %{groups_auth: false} = deployment_group} =
erpc_call(node, :toggle_groups_authorization, [deployment_group])
id = to_string(deployment_group.id)
assert_receive {:server_authorization_updated, %{id: ^id, groups_auth: false}}
assert conn
|> get(~p"/apps")
|> html_response(200) =~ slug
end
@tag :tmp_dir
test "shows all apps if disable the authentication in real-time",
%{conn: conn, node: node, code: code, tmp_dir: tmp_dir} = context do
{:ok, deployment_group} =
erpc_call(node, :toggle_groups_authorization, [context.deployment_group])
oidc_provider = erpc_call(node, :create_oidc_provider, [context.org])
authorization_group =
erpc_call(node, :create_authorization_group, [
%{
group_name: "marketing",
access_type: :apps,
prefixes: ["mkt-"],
oidc_provider: oidc_provider,
deployment_group: deployment_group
}
])
erpc_call(node, :update_user_info_groups, [
code,
[
%{
"provider_id" => to_string(oidc_provider.id),
"group_name" => authorization_group.group_name
}
]
])
slug = "marketing-app"
deploy_app(slug, context.team, context.org, context.deployment_group, tmp_dir, node)
assert conn
|> get(~p"/apps")
|> html_response(200) =~ "No apps running."
{:ok, %{teams_auth: false} = deployment_group} =
erpc_call(node, :toggle_teams_authentication, [deployment_group])
id = to_string(deployment_group.id)
assert_receive {:server_authorization_updated, %{id: ^id, teams_auth: false}}
assert conn
|> get(~p"/apps")
|> html_response(200) =~ slug
end
end
defp deploy_app(slug, team, org, deployment_group, tmp_dir, node) do
source = """
<!-- livebook:{"app_settings":{"access_type":"public","slug":"#{slug}"},"hub_id":"#{team.id}","deployment_group_id":"#{deployment_group.id}"} -->
# LivebookApp:#{slug}
```elixir
```
"""
{notebook, %{warnings: []}} = Livebook.LiveMarkdown.notebook_from_livemd(source)
files_dir = Livebook.FileSystem.File.local(tmp_dir)
{:ok, %Livebook.Teams.AppDeployment{file: zip_content} = app_deployment} =
Livebook.Teams.AppDeployment.new(notebook, files_dir)
secret_key = Livebook.Teams.derive_key(team.teams_key)
encrypted_content = Livebook.Teams.encrypt(zip_content, secret_key)
app_deployment_id =
erpc_call(node, :upload_app_deployment, [
org,
deployment_group,
app_deployment,
encrypted_content,
# broadcast?
true
]).id
app_deployment_id = to_string(app_deployment_id)
assert_receive {:app_deployment_started, %{id: ^app_deployment_id}}
assert_receive {:app_created, %{pid: pid, slug: ^slug}}
assert_receive {:app_updated,
%{
slug: ^slug,
sessions: [%{app_status: %{execution: :executed, lifecycle: :active}}]
}}
on_exit(fn ->
if Process.alive?(pid) do
Livebook.App.close(pid)
end
end)
end
end

View file

@ -73,7 +73,7 @@ defmodule Livebook.ZTA.LivebookTeamsTest do
} }
]) ])
{conn, code, %{restricted_apps_groups: []}} = authenticate_user(conn, node, test) {conn, code, %{groups: [], access_type: :apps}} = authenticate_user(conn, node, test)
session = get_session(conn) session = get_session(conn)
conn = conn =
@ -88,7 +88,7 @@ defmodule Livebook.ZTA.LivebookTeamsTest do
# Get the user with updated groups # Get the user with updated groups
erpc_call(node, :update_user_info_groups, [code, [group]]) erpc_call(node, :update_user_info_groups, [code, [group]])
assert {%{halted: false}, %{restricted_apps_groups: nil}} = assert {%{halted: false}, %{groups: [^group], access_type: :full}} =
LivebookTeams.authenticate(test, conn, []) LivebookTeams.authenticate(test, conn, [])
end end
@ -163,7 +163,7 @@ defmodule Livebook.ZTA.LivebookTeamsTest do
}} }}
# Now we need to check if the current user has access to this app # Now we need to check if the current user has access to this app
{conn, code, %{restricted_apps_groups: []}} = authenticate_user(conn, node, test) {conn, code, %{groups: [], access_type: :apps}} = authenticate_user(conn, node, test)
session = get_session(conn) session = get_session(conn)
group = %{ group = %{
@ -199,7 +199,7 @@ defmodule Livebook.ZTA.LivebookTeamsTest do
} }
]) ])
{conn, code, %{restricted_apps_groups: []}} = authenticate_user(conn, node, test) {conn, code, %{groups: [], access_type: :apps}} = authenticate_user(conn, node, test)
group = %{ group = %{
"provider_id" => to_string(oidc_provider.id), "provider_id" => to_string(oidc_provider.id),
@ -212,7 +212,7 @@ defmodule Livebook.ZTA.LivebookTeamsTest do
build_conn(:get, ~p"/settings") build_conn(:get, ~p"/settings")
|> init_test_session(get_session(conn)) |> init_test_session(get_session(conn))
assert {_conn, %{restricted_apps_groups: [^group]}} = assert {_conn, %{groups: [^group], access_type: :apps}} =
LivebookTeams.authenticate(test, conn, []) LivebookTeams.authenticate(test, conn, [])
end end

View file

@ -30,6 +30,12 @@ defmodule LivebookWeb.ConnCase do
end end
def with_authentication(conn, authentication) do def with_authentication(conn, authentication) do
Plug.Test.init_test_session(conn, authentication_test_override: authentication) Plug.Test.init_test_session(conn, %{authentication_test_override: authentication})
end
def with_authorization(conn, id) do
Plug.Test.init_test_session(conn, %{
identity_provider_test_override: {:zta, Livebook.ZTA.LivebookTeams, id}
})
end end
end end

View file

@ -1,8 +1,11 @@
defmodule Livebook.TeamsIntegrationCase do defmodule Livebook.TeamsIntegrationCase do
use ExUnit.CaseTemplate use ExUnit.CaseTemplate
import Phoenix.ConnTest
alias Livebook.TeamsServer alias Livebook.TeamsServer
@endpoint LivebookWeb.Endpoint
using do using do
quote do quote do
use Livebook.DataCase use Livebook.DataCase
@ -13,6 +16,7 @@ defmodule Livebook.TeamsIntegrationCase do
alias Livebook.TeamsServer alias Livebook.TeamsServer
import Livebook.HubHelpers import Livebook.HubHelpers
import Livebook.TeamsIntegrationCase
end end
end end
@ -31,4 +35,26 @@ defmodule Livebook.TeamsIntegrationCase do
{:ok, node: node, token: token, user: user} {:ok, node: node, token: token, user: user}
end end
def authenticate_user_on_teams(conn, node, team) do
response =
conn
|> LivebookWeb.ConnCase.with_authorization(team.id)
|> get("/")
|> html_response(200)
[_, location] = Regex.run(~r/URL\("(.*?)"\)/, response)
uri = URI.parse(location)
%{"code" => code} = URI.decode_query(uri.query)
Livebook.HubHelpers.erpc_call(node, :allow_auth_request, [code])
session =
conn
|> LivebookWeb.ConnCase.with_authorization(team.id)
|> get("/", %{teams_identity: "", code: code})
|> Plug.Conn.get_session()
{Plug.Test.init_test_session(conn, session), code}
end
end end