Skip to content

Implement the user authorization flow with Livebook Teams #2984

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

aleDsz
Copy link
Member

@aleDsz aleDsz commented Apr 13, 2025

No description provided.

@aleDsz aleDsz self-assigned this Apr 13, 2025
Copy link

github-actions bot commented Apr 13, 2025

Uffizzi Ephemeral Environment deployment-62733

☁️ https://app.uffizzi.com/github.com/livebook-dev/livebook/pull/2984

📄 View Application Logs etc.

What is Uffizzi? Learn more!

Comment on lines +312 to +322
cond do
# If the user have full access, it should be verified first
authorized_group?(deployment_group.authorization_groups, user.groups) ->
{:reply, true, state}

authorized_group?(app_deployment.authorization_groups, user.groups) ->
{:reply, true, state}

true ->
{:reply, false, state}
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
cond do
# If the user have full access, it should be verified first
authorized_group?(deployment_group.authorization_groups, user.groups) ->
{:reply, true, state}
authorized_group?(app_deployment.authorization_groups, user.groups) ->
{:reply, true, state}
true ->
{:reply, false, state}
end
app_access? =
authorized_group?(deployment_group.authorization_groups, user.groups) or
authorized_group?(app_deployment.authorization_groups, user.groups)
{:reply, app_access?, state}

# Get the user with updated groups
erpc_call(node, :update_user_info_groups, [code, [group]])
assert {%{halted: true} = conn, nil} = LivebookTeams.authenticate(test, conn, [])
assert html_response(conn, 200) =~ "You don't have permission to access this app"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The status should be 401, same in other cases :)


erpc_call(node, :update_user_info_groups, [code, [group]])

for page <- ["/settings", "/learn", "/hub", "/apps-dashboard"] do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine to test just one of those pages, we want to test a category of pages rather than specific pages.

app_deployment_id = to_string(app_deployment_id)
assert_receive {:app_deployment_started, %{id: ^app_deployment_id}}

Livebook.Apps.Manager.sync_permanent_apps()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Syncing should happen automatically on new deployment, we shouldn't have to do it programmatically?

@@ -29,7 +35,7 @@ defmodule LivebookWeb.ErrorHTML do
"""
end

attr :status, :integer, required: true
attr :status, :any, required: true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's pick :integer or :string and pass it like that in all cases for consistency.

end

defp authorize_user({%{path_info: ["apps", slug | _]} = conn, metadata}, team) do
if Livebook.Apps.exists?(slug) do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to check this upfront? If the app doesn't exist, a redirect should happen down the line and that would again check for full access?

Comment on lines +1006 to 1015
defp manager_sync(%{deployment_group_id: id}, state) do
with {:ok, deployment_group} <- fetch_deployment_group(id, state) do
if deployment_group.id == state.deployment_group_id do
# Each node runs the teams client, but we only need to call sync once
if Apps.Manager.local?() do
Apps.Manager.sync_permanent_apps()
end
end
end
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if there was a good reason to call fetch_deployment_group, I think we can just compare the ids:

Suggested change
defp manager_sync(%{deployment_group_id: id}, state) do
with {:ok, deployment_group} <- fetch_deployment_group(id, state) do
if deployment_group.id == state.deployment_group_id do
# Each node runs the teams client, but we only need to call sync once
if Apps.Manager.local?() do
Apps.Manager.sync_permanent_apps()
end
end
end
end
defp manager_sync(app_deployment, state) do
# We only need to sync if the app deployment belongs to the current
# deployment group
if app_deployment.deployment_group_id == state.deployment_group_id do
# Each node runs the teams client, but we only need to call sync once
if Apps.Manager.local?() do
Apps.Manager.sync_permanent_apps()
end
end
end

Comment on lines +116 to +123
defp authorize_user({%{halted: true} = conn, metadata}, _team) do
{conn, metadata}
end

defp authorize_user({%{path_info: path_info} = conn, metadata}, _team)
when path_info in [[], ["apps"]] do
{conn, metadata}
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this should be a part of the ZTA implementation. The purpose behind ZTA plug is very focused.

For apps we have AppAuthHook, so we could have the extra authorization there. For the full access, we may need a separate plug/hook.

@josevalim wdyt?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I deleted my previous comments to avoid confusion.

Yes, you are correct. Here is my suggestion, rename user.groups to user.restricted_apps_groups. Then I think the logic here should be this:

  1. If the user has full access, pass the user forward with user.restricted_apps_groups == nil
  2. If the user does not have full access, set user.restricted_apps_groups to the groups
  3. In AuthPlug check if user.restricted_apps_groups != nil, if it is, they can only access apps, and therefore we should fail
  4. In AppAuthHook check if user.restricted_apps_groups != nil, if so, it only access if the user belongs to the app groups

In other words, the goal of the ZTA is to populate restrict_apps_groupsaccordingly, the other plugs redirect.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good!

@@ -3,13 +3,13 @@ defmodule LivebookWeb.ErrorHTML do

def render("404.html", assigns) do
~H"""
<.error_page status={@status} title="No Numbats here!" />
<.error_page status="404" title="No Numbats here!" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<.error_page status="404" title="No Numbats here!" />
<.error_page status={404} title="No Numbats here!" />

So we always pass integer :)

# With this strategy, we guarantee that unauthorized users
# won't be able to keep reading the app which they
# should't have access.
{:noreply, redirect(socket, to: ~p"/apps/#{slug}/sessions/#{socket.assigns.session.id}")}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can cause the user to lose some state, like something they passed in the form. Could we redirect only if they lose access?

defp build_authorization_groups(%{authorization_groups: authorization_groups}) do
for authorization_group <- authorization_groups do
%Teams.AuthorizationGroup{
oidc_provider_id: authorization_group.oidc_provider_id,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's call it provider_id both on server and here, this will make it easier if somehow we need to support authorization groups from other providers in the future. :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I can see Livebook Teams being a provider of authorization groups in the future, for example.

@livebook-dev livebook-dev deleted a comment from jonatanklosko Apr 14, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants