diff --git a/lib/livebook/apps.ex b/lib/livebook/apps.ex index 912dbe315..b8260a9a6 100644 --- a/lib/livebook/apps.ex +++ b/lib/livebook/apps.ex @@ -7,6 +7,7 @@ defmodule Livebook.Apps do require Logger alias Livebook.App + alias Livebook.Apps @doc """ 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() def authorized?(app, user) - def authorized?(%{app_spec: %Livebook.Apps.TeamsAppSpec{}}, %{restricted_apps_groups: []}), - do: false + def authorized?(_app, %{access_type: :full}), do: true - def authorized?(_app, %{restricted_apps_groups: nil}), do: true - - 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) + def authorized?(%{slug: slug, app_spec: %Apps.TeamsAppSpec{hub_id: id}}, user) do + Livebook.Hubs.TeamClient.user_app_access?(id, user.groups, slug) end @doc """ diff --git a/lib/livebook/hubs/team_client.ex b/lib/livebook/hubs/team_client.ex index 7dc9e14da..de70f88c8 100644 --- a/lib/livebook/hubs/team_client.ex +++ b/lib/livebook/hubs/team_client.ex @@ -296,26 +296,39 @@ defmodule Livebook.Hubs.TeamClient do end end - def handle_call({:check_full_access, groups}, _caller, %{deployment_group_id: id} = state) do - case fetch_deployment_group(id, state) do - {:ok, deployment_group} -> - {:reply, authorized_group?(deployment_group.authorization_groups, groups), state} + def handle_call({:check_full_access, groups}, _caller, state) do + if id = state.deployment_group_id do + case fetch_deployment_group(id, state) do + {:ok, deployment_group} -> + {: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 + else + {:reply, true, state} end end - def handle_call({:check_app_access, groups, slug}, _caller, %{deployment_group_id: id} = state) do - with {:ok, deployment_group} <- fetch_deployment_group(id, state), - {:ok, app_deployment} <- fetch_app_deployment_from_slug(slug, state) do - app_access? = - authorized_group?(deployment_group.authorization_groups, groups) or - authorized_group?(app_deployment.authorization_groups, groups) + 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), + {:ok, app_deployment} <- fetch_app_deployment_from_slug(slug, state) do + app_access? = + not deployment_group.teams_auth or + 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 + _ -> {:reply, false, state} + end else - _ -> {:reply, false, state} + {:reply, true, state} end end @@ -492,6 +505,7 @@ defmodule Livebook.Hubs.TeamClient do clustering: nullify(deployment_group.clustering), url: nullify(deployment_group.url), teams_auth: deployment_group.teams_auth, + groups_auth: deployment_group.groups_auth, authorization_groups: authorization_groups } end @@ -531,6 +545,7 @@ defmodule Livebook.Hubs.TeamClient do clustering: atomize(deployment_group_updated.clustering), url: nullify(deployment_group_updated.url), teams_auth: deployment_group_updated.teams_auth, + groups_auth: deployment_group_updated.groups_auth, authorization_groups: authorization_groups } end @@ -659,7 +674,6 @@ defmodule Livebook.Hubs.TeamClient do defp handle_event(:deployment_group_created, %Teams.DeploymentGroup{} = deployment_group, state) do Teams.Broadcasts.deployment_group_created(deployment_group) - put_deployment_group(state, deployment_group) end @@ -673,6 +687,16 @@ defmodule Livebook.Hubs.TeamClient do defp handle_event(:deployment_group_updated, %Teams.DeploymentGroup{} = deployment_group, state) do 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) end diff --git a/lib/livebook/teams/broadcasts.ex b/lib/livebook/teams/broadcasts.ex index f1215e50c..590af23bd 100644 --- a/lib/livebook/teams/broadcasts.ex +++ b/lib/livebook/teams/broadcasts.ex @@ -7,6 +7,7 @@ defmodule Livebook.Teams.Broadcasts do @app_deployments_topic "teams:app_deployments" @clients_topic "teams:clients" @deployment_groups_topic "teams:deployment_groups" + @app_server_topic "teams:app_server" @doc """ Subscribes to one or more subtopics in `"teams"`. @@ -31,9 +32,13 @@ defmodule Livebook.Teams.Broadcasts do Topic `#{@deployment_groups_topic}`: * `{:deployment_group_created, DeploymentGroup.t()}` - * `{:deployment_group_update, DeploymentGroup.t()}` + * `{:deployment_group_updated, DeploymentGroup.t()}` * `{:deployment_group_deleted, DeploymentGroup.t()}` + Topic `#{@app_server_topic}`: + + * `{:server_authorization_updated, DeploymentGroup.t()}` + """ @spec subscribe(atom() | list(atom())) :: :ok | {:error, term()} def subscribe(topics) when is_list(topics) do @@ -132,6 +137,14 @@ defmodule Livebook.Teams.Broadcasts do broadcast(@agents_topic, {:agent_left, agent}) 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 Phoenix.PubSub.broadcast(Livebook.PubSub, topic, message) end diff --git a/lib/livebook/teams/deployment_group.ex b/lib/livebook/teams/deployment_group.ex index f63852bf6..606140fdf 100644 --- a/lib/livebook/teams/deployment_group.ex +++ b/lib/livebook/teams/deployment_group.ex @@ -13,6 +13,7 @@ defmodule Livebook.Teams.DeploymentGroup do clustering: :auto | :dns | nil, hub_id: String.t() | nil, teams_auth: boolean(), + groups_auth: boolean(), authorization_groups: Ecto.Schema.embeds_many(Teams.AuthorizationGroup.t()), secrets: Ecto.Schema.has_many(Secrets.Secret.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 :url, :string field :teams_auth, :boolean, default: true + field :groups_auth, :boolean, default: false has_many :secrets, Secrets.Secret has_many :agent_keys, Teams.AgentKey @@ -36,7 +38,7 @@ defmodule Livebook.Teams.DeploymentGroup do def changeset(deployment_group, attrs \\ %{}) do 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]) |> update_change(:url, fn url -> if url do diff --git a/lib/livebook/teams/requests.ex b/lib/livebook/teams/requests.ex index c9121c2c8..298e7f0b4 100644 --- a/lib/livebook/teams/requests.ex +++ b/lib/livebook/teams/requests.ex @@ -172,8 +172,7 @@ defmodule Livebook.Teams.Requests do name: deployment_group.name, mode: deployment_group.mode, clustering: deployment_group.clustering, - url: deployment_group.url, - teams_auth: deployment_group.teams_auth + url: deployment_group.url } post("/api/v1/org/deployment-groups", params, team) diff --git a/lib/livebook/users/user.ex b/lib/livebook/users/user.ex index 52d33b422..3745cccc3 100644 --- a/lib/livebook/users/user.ex +++ b/lib/livebook/users/user.ex @@ -12,12 +12,15 @@ defmodule Livebook.Users.User do alias Livebook.Utils + @type access_type :: :full | :apps + @type t :: %__MODULE__{ id: id(), name: String.t() | nil, email: 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, hex_color: hex_color() } @@ -29,7 +32,8 @@ defmodule Livebook.Users.User do field :name, :string field :email, :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 :hex_color, Livebook.EctoTypes.HexColor end @@ -44,15 +48,17 @@ defmodule Livebook.Users.User do name: nil, email: nil, avatar_url: nil, - restricted_apps_groups: nil, + access_type: :full, + groups: [], payload: nil, hex_color: Livebook.EctoTypes.HexColor.random() } end + @doc false def changeset(user, attrs \\ %{}) do 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]) end end diff --git a/lib/livebook/zta.ex b/lib/livebook/zta.ex index a6696ab26..1baa1babc 100644 --- a/lib/livebook/zta.ex +++ b/lib/livebook/zta.ex @@ -58,6 +58,9 @@ defmodule Livebook.ZTA do * `:id` - a string that uniquely identifies the user * `:name` - the user name * `:email` - the user email + * `:avatar_url` - the user avatar + * `:access_type` - the user access type + * `:groups` - the user groups * `:payload` - the provider payload 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(:name) => 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() } diff --git a/lib/livebook/zta/livebook_teams.ex b/lib/livebook/zta/livebook_teams.ex index ebc578c9a..6dd2117ef 100644 --- a/lib/livebook/zta/livebook_teams.ex +++ b/lib/livebook/zta/livebook_teams.ex @@ -159,31 +159,36 @@ defmodule Livebook.ZTA.LivebookTeams do defp get_user_info(team, access_token) do with {:ok, payload} <- Teams.Requests.get_user_info(team, access_token) do - %{ - "id" => id, - "name" => name, - "email" => email, - "groups" => groups, - "avatar_url" => avatar_url - } = payload - - restricted_apps_groups = - if Livebook.Hubs.TeamClient.user_full_access?(team.id, groups) do - nil - else - groups - end - - metadata = %{ - id: id, - name: name, - avatar_url: avatar_url, - restricted_apps_groups: restricted_apps_groups, - email: email, - payload: payload - } - - {:ok, metadata} + {: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, + "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 diff --git a/lib/livebook_web/live/app_live.ex b/lib/livebook_web/live/app_live.ex index 513a223d8..2af9c21f3 100644 --- a/lib/livebook_web/live/app_live.ex +++ b/lib/livebook_web/live/app_live.ex @@ -12,8 +12,13 @@ defmodule LivebookWeb.AppLive do end end - def mount(_params, _session, socket) when not socket.assigns.app_authorized? do - {:ok, socket, layout: false} + def mount(%{"slug" => slug}, _session, socket) when not socket.assigns.app_authorized? do + 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 def mount(%{"slug" => slug}, _session, socket) do @@ -22,6 +27,7 @@ defmodule LivebookWeb.AppLive do if connected?(socket) do Livebook.App.subscribe(slug) + Livebook.Teams.Broadcasts.subscribe(:app_deployments) end {:ok, assign(socket, app: app)} @@ -122,6 +128,18 @@ defmodule LivebookWeb.AppLive do {:noreply, assign(socket, :app, app)} 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} defp active_sessions(sessions) do diff --git a/lib/livebook_web/live/app_session_live.ex b/lib/livebook_web/live/app_session_live.ex index 87a3c3cbb..b960ab6fe 100644 --- a/lib/livebook_web/live/app_session_live.ex +++ b/lib/livebook_web/live/app_session_live.ex @@ -24,8 +24,13 @@ defmodule LivebookWeb.AppSessionLive do end end - def mount(_params, _session, socket) when not socket.assigns.app_authorized? do - {:ok, socket, layout: false} + def mount(%{"slug" => slug, "id" => session_id}, _session, socket) + 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 def mount(%{"slug" => slug, "id" => session_id}, _session, socket) do @@ -388,7 +393,8 @@ defmodule LivebookWeb.AppSessionLive do # should't have access. {: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} else {:noreply, redirect(socket, to: ~p"/apps/#{slug}/sessions/#{socket.assigns.session.id}")} diff --git a/lib/livebook_web/live/apps_live.ex b/lib/livebook_web/live/apps_live.ex index fc865bfda..445458965 100644 --- a/lib/livebook_web/live/apps_live.ex +++ b/lib/livebook_web/live/apps_live.ex @@ -4,7 +4,7 @@ defmodule LivebookWeb.AppsLive do @impl true def mount(_params, _session, socket) do if connected?(socket) do - Livebook.Teams.Broadcasts.subscribe(:app_deployments) + Livebook.Teams.Broadcasts.subscribe(:app_server) Livebook.Apps.subscribe() end @@ -101,7 +101,7 @@ defmodule LivebookWeb.AppsLive do {:noreply, update(socket, :apps, &LivebookWeb.AppComponents.update_app_list(&1, event))} 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) {:noreply, assign(socket, :apps, apps)} end diff --git a/lib/livebook_web/live/hooks/auth_hook.ex b/lib/livebook_web/live/hooks/auth_hook.ex index 8696d5fa1..4c8a94a89 100644 --- a/lib/livebook_web/live/hooks/auth_hook.ex +++ b/lib/livebook_web/live/hooks/auth_hook.ex @@ -5,6 +5,7 @@ defmodule LivebookWeb.AuthHook do def on_mount(:default, _params, session, socket) do 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 {:cont, socket} @@ -12,4 +13,19 @@ defmodule LivebookWeb.AuthHook do {:halt, redirect(socket, to: ~p"/")} 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 diff --git a/lib/livebook_web/live/hooks/user_hook.ex b/lib/livebook_web/live/hooks/user_hook.ex index ff62fcad4..04b654be7 100644 --- a/lib/livebook_web/live/hooks/user_hook.ex +++ b/lib/livebook_web/live/hooks/user_hook.ex @@ -1,4 +1,6 @@ defmodule LivebookWeb.UserHook do + use LivebookWeb, :verified_routes + import Phoenix.Component import Phoenix.LiveView @@ -13,10 +15,12 @@ defmodule LivebookWeb.UserHook do user_data = connect_params["user_data"] || session["user_data"] LivebookWeb.UserPlug.build_current_user(session, identity_data, user_data) 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 Livebook.Users.subscribe(socket.assigns.current_user.id) + Livebook.Teams.Broadcasts.subscribe(:app_server) end Logger.metadata(Livebook.Utils.logger_users_metadata([socket.assigns.current_user])) @@ -24,12 +28,25 @@ defmodule LivebookWeb.UserHook do {:cont, socket} end - defp info( + defp handle_info( {:user_change, %{id: id} = user}, %{assigns: %{current_user: %{id: id}}} = socket ) do {:halt, assign(socket, :current_user, user)} 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 diff --git a/lib/livebook_web/plugs/auth_plug.ex b/lib/livebook_web/plugs/auth_plug.ex index 44d17c8f6..565d8bd40 100644 --- a/lib/livebook_web/plugs/auth_plug.ex +++ b/lib/livebook_web/plugs/auth_plug.ex @@ -64,7 +64,7 @@ defmodule LivebookWeb.AuthPlug do get_session(conn), conn.assigns.identity_data, conn.assigns.user_data - ).restricted_apps_groups == nil + ).access_type == :full end @doc """ @@ -77,7 +77,7 @@ defmodule LivebookWeb.AuthPlug do session, session["identity_data"], session["user_data"] - ).restricted_apps_groups == nil + ).access_type == :full end defp authenticate(conn) do @@ -136,6 +136,7 @@ defmodule LivebookWeb.AuthPlug do conn |> put_status(:unauthorized) |> put_view(LivebookWeb.ErrorHTML) + |> put_root_layout(false) |> render("401.html", %{details: "You don't have permission to access this server"}) |> halt() end diff --git a/lib/livebook_web/plugs/user_plug.ex b/lib/livebook_web/plugs/user_plug.ex index 06e5d2878..06581f7ad 100644 --- a/lib/livebook_web/plugs/user_plug.ex +++ b/lib/livebook_web/plugs/user_plug.ex @@ -34,7 +34,7 @@ defmodule LivebookWeb.UserPlug do end 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, []) cond do @@ -128,4 +128,22 @@ defmodule LivebookWeb.UserPlug do "user_data" => conn.assigns.user_data } 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 diff --git a/proto/lib/livebook_proto/deployment_group.pb.ex b/proto/lib/livebook_proto/deployment_group.pb.ex index 17de2ca14..5e72b4339 100644 --- a/proto/lib/livebook_proto/deployment_group.pb.ex +++ b/proto/lib/livebook_proto/deployment_group.pb.ex @@ -22,4 +22,6 @@ defmodule LivebookProto.DeploymentGroup do repeated: true, type: LivebookProto.AuthorizationGroup, json_name: "authorizationGroups" + + field :groups_auth, 13, type: :bool, json_name: "groupsAuth" end diff --git a/proto/lib/livebook_proto/deployment_group_updated.pb.ex b/proto/lib/livebook_proto/deployment_group_updated.pb.ex index d5c99b8d7..213be9eec 100644 --- a/proto/lib/livebook_proto/deployment_group_updated.pb.ex +++ b/proto/lib/livebook_proto/deployment_group_updated.pb.ex @@ -21,4 +21,6 @@ defmodule LivebookProto.DeploymentGroupUpdated do repeated: true, type: LivebookProto.AuthorizationGroup, json_name: "authorizationGroups" + + field :groups_auth, 12, type: :bool, json_name: "groupsAuth" end diff --git a/proto/messages.proto b/proto/messages.proto index 9a6ffc1c7..c648673b7 100644 --- a/proto/messages.proto +++ b/proto/messages.proto @@ -67,6 +67,7 @@ message DeploymentGroup { repeated EnvironmentVariable environment_variables = 10; bool teams_auth = 11; repeated AuthorizationGroup authorization_groups = 12; + bool groups_auth = 13; } message DeploymentGroupCreated { @@ -93,6 +94,7 @@ message DeploymentGroupUpdated { repeated EnvironmentVariable environment_variables = 9; bool teams_auth = 10; repeated AuthorizationGroup authorization_groups = 11; + bool groups_auth = 12; } message DeploymentGroupDeleted { diff --git a/test/livebook_teams/web/admin_live_test.exs b/test/livebook_teams/web/admin_live_test.exs index f171dd657..5df848cbc 100644 --- a/test/livebook_teams/web/admin_live_test.exs +++ b/test/livebook_teams/web/admin_live_test.exs @@ -4,28 +4,207 @@ defmodule LivebookWeb.Integration.AdminLiveTest do import Phoenix.LiveViewTest - setup %{teams_auth: teams_auth} do - Application.put_env(:livebook, :teams_auth, teams_auth) - on_exit(fn -> Application.delete_env(:livebook, :teams_auth) end) + describe "topbar" do + setup %{teams_auth: teams_auth} do + Application.put_env(:livebook, :teams_auth, teams_auth) + on_exit(fn -> Application.delete_env(:livebook, :teams_auth) end) - :ok - end - - for page <- ["/", "/settings", "/learn", "/hub", "/apps-dashboard"] do - @tag page: page, teams_auth: :online - test "GET #{page} shows the app server instance topbar warning", %{conn: conn, page: page} do - {:ok, view, _} = live(conn, page) - - assert render(view) =~ - "This Livebook instance has been configured for notebook deployment and is in read-only mode." + :ok end - @tag page: page, teams_auth: :offline - test "GET #{page} shows the offline hub topbar warning", %{conn: conn, page: page} do - {:ok, view, _} = live(conn, page) + for page <- ["/", "/settings", "/learn", "/hub", "/apps-dashboard"] do + @tag page: page, teams_auth: :online + test "GET #{page} shows the app server instance topbar warning", %{conn: conn, page: page} do + {:ok, view, _} = live(conn, page) - assert render(view) =~ - "You are running an offline Workspace for deployment. You cannot modify its settings." + assert render(view) =~ + "This Livebook instance has been configured for notebook deployment and is in read-only mode." + end + + @tag page: page, teams_auth: :offline + test "GET #{page} shows the offline hub topbar warning", %{conn: conn, page: page} do + {:ok, view, _} = live(conn, page) + + assert render(view) =~ + "You are running an offline Workspace for deployment. You cannot modify its settings." + 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 diff --git a/test/livebook_teams/web/app_session_live_test.exs b/test/livebook_teams/web/app_session_live_test.exs new file mode 100644 index 000000000..f1e2d91e2 --- /dev/null +++ b/test/livebook_teams/web/app_session_live_test.exs @@ -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 = """ + + + # 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 diff --git a/test/livebook_teams/web/apps_live_test.exs b/test/livebook_teams/web/apps_live_test.exs new file mode 100644 index 000000000..a1ad7bcbf --- /dev/null +++ b/test/livebook_teams/web/apps_live_test.exs @@ -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 = """ + + + # 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 diff --git a/test/livebook_teams/zta/livebook_teams_test.exs b/test/livebook_teams/zta/livebook_teams_test.exs index 31798cdc1..a043d0de2 100644 --- a/test/livebook_teams/zta/livebook_teams_test.exs +++ b/test/livebook_teams/zta/livebook_teams_test.exs @@ -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) conn = @@ -88,7 +88,7 @@ defmodule Livebook.ZTA.LivebookTeamsTest do # Get the user with updated groups 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, []) end @@ -163,7 +163,7 @@ defmodule Livebook.ZTA.LivebookTeamsTest do }} # 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) 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 = %{ "provider_id" => to_string(oidc_provider.id), @@ -212,7 +212,7 @@ defmodule Livebook.ZTA.LivebookTeamsTest do build_conn(:get, ~p"/settings") |> init_test_session(get_session(conn)) - assert {_conn, %{restricted_apps_groups: [^group]}} = + assert {_conn, %{groups: [^group], access_type: :apps}} = LivebookTeams.authenticate(test, conn, []) end diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index c935d86a7..847ad6df8 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -30,6 +30,12 @@ defmodule LivebookWeb.ConnCase do end 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 diff --git a/test/support/teams_integration_case.ex b/test/support/teams_integration_case.ex index cc6309782..8b83d0093 100644 --- a/test/support/teams_integration_case.ex +++ b/test/support/teams_integration_case.ex @@ -1,8 +1,11 @@ defmodule Livebook.TeamsIntegrationCase do use ExUnit.CaseTemplate + import Phoenix.ConnTest alias Livebook.TeamsServer + @endpoint LivebookWeb.Endpoint + using do quote do use Livebook.DataCase @@ -13,6 +16,7 @@ defmodule Livebook.TeamsIntegrationCase do alias Livebook.TeamsServer import Livebook.HubHelpers + import Livebook.TeamsIntegrationCase end end @@ -31,4 +35,26 @@ defmodule Livebook.TeamsIntegrationCase do {:ok, node: node, token: token, user: user} 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