-
Notifications
You must be signed in to change notification settings - Fork 456
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
base: main
Are you sure you want to change the base?
Conversation
Uffizzi Ephemeral Environment
|
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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" |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
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 |
There was a problem hiding this comment.
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:
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 |
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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:
- If the user has full access, pass the user forward with user.restricted_apps_groups == nil
- If the user does not have full access, set
user.restricted_apps_groups
to the groups - In AuthPlug check if
user.restricted_apps_groups != nil
, if it is, they can only access apps, and therefore we should fail - 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_groups
accordingly, the other plugs redirect.
There was a problem hiding this comment.
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!" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<.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}")} |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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. :)
There was a problem hiding this comment.
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.
No description provided.