Skip to content

Commit 040836f

Browse files
committed
Check user authorization before accessing the page
1 parent c9698eb commit 040836f

File tree

2 files changed

+251
-40
lines changed

2 files changed

+251
-40
lines changed

lib/livebook/zta/livebook_teams.ex

+55-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,9 @@ defmodule Livebook.ZTA.LivebookTeams do
7979
defp handle_request(conn, team, _params) do
8080
case get_session(conn) do
8181
%{"livebook_teams_access_token" => access_token} ->
82-
validate_access_token(conn, team, access_token)
82+
conn
83+
|> validate_access_token(team, access_token)
84+
|> authorize_user(team)
8385

8486
# it means, we couldn't reach to Teams server
8587
%{"teams_error" => true} ->
@@ -111,6 +113,27 @@ defmodule Livebook.ZTA.LivebookTeams do
111113
end
112114
end
113115

116+
defp authorize_user({%{halted: true} = conn, metadata}, _team) do
117+
{conn, metadata}
118+
end
119+
120+
defp authorize_user({%{path_info: path_info} = conn, metadata}, _team)
121+
when path_info in [[], ["apps"]] do
122+
{conn, metadata}
123+
end
124+
125+
defp authorize_user({%{path_info: ["apps", slug | _]} = conn, metadata}, team) do
126+
if Livebook.Apps.exists?(slug) do
127+
check_app_access(conn, metadata, slug, team)
128+
else
129+
check_full_access(conn, metadata, team)
130+
end
131+
end
132+
133+
defp authorize_user({conn, metadata}, team) do
134+
check_full_access(conn, metadata, team)
135+
end
136+
114137
defp retrieve_access_token(team, code) do
115138
with {:ok, %{"access_token" => access_token}} <-
116139
Teams.Requests.retrieve_access_token(team, code) do
@@ -177,4 +200,35 @@ defmodule Livebook.ZTA.LivebookTeams do
177200
{:ok, metadata}
178201
end
179202
end
203+
204+
defp check_full_access(conn, metadata, team) do
205+
if Livebook.Hubs.TeamClient.user_full_access?(team.id, get_user!(metadata)) do
206+
{conn, metadata}
207+
else
208+
{conn
209+
|> put_view(LivebookWeb.ErrorHTML)
210+
|> render("401.html", %{details: "You don't have permission to access this server"})
211+
|> halt(), nil}
212+
end
213+
end
214+
215+
defp check_app_access(conn, metadata, slug, team) do
216+
if Livebook.Hubs.TeamClient.user_app_access?(team.id, get_user!(metadata), slug) do
217+
{conn, metadata}
218+
else
219+
{conn
220+
|> put_view(LivebookWeb.ErrorHTML)
221+
|> render("401.html", %{details: "You don't have permission to access this app"})
222+
|> halt(), nil}
223+
end
224+
end
225+
226+
defp get_user!(metadata) do
227+
{:ok, user} =
228+
metadata.id
229+
|> Livebook.Users.User.new()
230+
|> Livebook.Users.update_user(metadata)
231+
232+
user
233+
end
180234
end

test/livebook_teams/zta/livebook_teams_test.exs

+196-39
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ defmodule Livebook.ZTA.LivebookTeamsTest do
1616
%{hub_id: ^hub_id, org_id: ^org_id, deployment_group_id: ^deployment_group_id}}
1717

1818
start_supervised!({LivebookTeams, name: test, identity_key: team.id})
19-
{:ok, deployment_group: deployment_group, team: team}
19+
{:ok, deployment_group: deployment_group, org: org, team: team}
2020
end
2121

2222
describe "authenticate/3" do
@@ -58,6 +58,171 @@ defmodule Livebook.ZTA.LivebookTeamsTest do
5858
assert {%{halted: false}, ^metadata} = LivebookTeams.authenticate(test, conn, [])
5959
end
6060

61+
test "authorizes user to access admin page with full access permission",
62+
%{conn: conn, node: node, deployment_group: deployment_group, org: org, test: test} do
63+
erpc_call(node, :toggle_groups_authorization, [deployment_group])
64+
oidc_provider = erpc_call(node, :create_oidc_provider, [org])
65+
66+
authorization_group =
67+
erpc_call(node, :create_authorization_group, [
68+
%{
69+
group_name: "developers",
70+
access_type: :app_server,
71+
oidc_provider: oidc_provider,
72+
deployment_group: deployment_group
73+
}
74+
])
75+
76+
{conn, code, %{groups: []}} = authenticate_user(conn, node, test)
77+
session = get_session(conn)
78+
79+
conn =
80+
build_conn(:get, ~p"/")
81+
|> init_test_session(session)
82+
83+
group = %{
84+
"oidc_provider_id" => to_string(oidc_provider.id),
85+
"group_name" => authorization_group.group_name
86+
}
87+
88+
# Get the user with updated groups
89+
erpc_call(node, :update_user_info_groups, [code, [group]])
90+
assert {%{halted: false}, %{groups: [^group]}} = LivebookTeams.authenticate(test, conn, [])
91+
end
92+
93+
@tag :tmp_dir
94+
test "renders unauthorized user to access app with prefix the user don't have access",
95+
%{
96+
conn: conn,
97+
node: node,
98+
deployment_group: deployment_group,
99+
org: org,
100+
tmp_dir: tmp_dir,
101+
team: team,
102+
test: test
103+
} do
104+
erpc_call(node, :toggle_groups_authorization, [deployment_group])
105+
oidc_provider = erpc_call(node, :create_oidc_provider, [org])
106+
107+
authorization_group =
108+
erpc_call(node, :create_authorization_group, [
109+
%{
110+
group_name: "marketing",
111+
access_type: :apps,
112+
prefixes: ["mkt-"],
113+
oidc_provider: oidc_provider,
114+
deployment_group: deployment_group
115+
}
116+
])
117+
118+
Livebook.Apps.subscribe()
119+
slug = "marketing-app"
120+
121+
notebook = %{
122+
Livebook.Notebook.new()
123+
| app_settings: %{Livebook.Notebook.AppSettings.new() | slug: slug},
124+
file_entries: [],
125+
name: slug,
126+
hub_id: team.id,
127+
deployment_group_id: to_string(deployment_group.id)
128+
}
129+
130+
files_dir = Livebook.FileSystem.File.local(tmp_dir)
131+
132+
{:ok, %Livebook.Teams.AppDeployment{file: zip_content} = app_deployment} =
133+
Livebook.Teams.AppDeployment.new(notebook, files_dir)
134+
135+
secret_key = Livebook.Teams.derive_key(team.teams_key)
136+
encrypted_content = Livebook.Teams.encrypt(zip_content, secret_key)
137+
138+
Livebook.Teams.Broadcasts.subscribe(:app_deployments)
139+
140+
app_deployment_id =
141+
erpc_call(node, :upload_app_deployment, [
142+
org,
143+
deployment_group,
144+
app_deployment,
145+
encrypted_content,
146+
# broadcast?
147+
true
148+
]).id
149+
150+
app_deployment_id = to_string(app_deployment_id)
151+
assert_receive {:app_deployment_started, %{id: ^app_deployment_id}}
152+
153+
Livebook.Apps.Manager.sync_permanent_apps()
154+
Livebook.Apps.subscribe()
155+
156+
assert_receive {:app_created, %{pid: pid, slug: ^slug}}
157+
158+
assert_receive {:app_updated,
159+
%{
160+
slug: ^slug,
161+
sessions: [%{app_status: %{execution: :executed, lifecycle: :active}}]
162+
}}
163+
164+
# Now we need to check if the current user has access to this app
165+
{conn, code, %{groups: []}} = authenticate_user(conn, node, test)
166+
session = get_session(conn)
167+
168+
conn =
169+
build_conn(:get, ~p"/apps/#{slug}")
170+
|> init_test_session(session)
171+
172+
group = %{
173+
"oidc_provider_id" => to_string(oidc_provider.id),
174+
"group_name" => authorization_group.group_name
175+
}
176+
177+
# Get the user with updated groups
178+
erpc_call(node, :update_user_info_groups, [code, [group]])
179+
assert {%{halted: true} = conn, nil} = LivebookTeams.authenticate(test, conn, [])
180+
assert html_response(conn, 200) =~ "You don&#39;t have permission to access this app"
181+
182+
# Guarantee we don't list the app for this user
183+
conn = build_conn(:get, ~p"/") |> init_test_session(session)
184+
{%{halted: false}, metadata} = LivebookTeams.authenticate(test, conn, [])
185+
{:ok, user} = Livebook.Users.update_user(Livebook.Users.User.new(metadata.id), metadata)
186+
assert Livebook.Apps.list_authorized_apps(user) == []
187+
188+
Livebook.App.close(pid)
189+
end
190+
191+
test "renders unauthorized user to access admin page with slug prefix access permission",
192+
%{conn: conn, node: node, deployment_group: deployment_group, org: org, test: test} do
193+
erpc_call(node, :toggle_groups_authorization, [deployment_group])
194+
oidc_provider = erpc_call(node, :create_oidc_provider, [org])
195+
196+
authorization_group =
197+
erpc_call(node, :create_authorization_group, [
198+
%{
199+
group_name: "marketing",
200+
access_type: :apps,
201+
prefixes: ["mkt-"],
202+
oidc_provider: oidc_provider,
203+
deployment_group: deployment_group
204+
}
205+
])
206+
207+
{conn, code, %{groups: []}} = authenticate_user(conn, node, test)
208+
209+
group = %{
210+
"oidc_provider_id" => to_string(oidc_provider.id),
211+
"group_name" => authorization_group.group_name
212+
}
213+
214+
erpc_call(node, :update_user_info_groups, [code, [group]])
215+
216+
for page <- ["/settings", "/learn", "/hub", "/apps-dashboard"] do
217+
conn =
218+
build_conn(:get, page)
219+
|> init_test_session(get_session(conn))
220+
221+
assert {%{halted: true} = conn, nil} = LivebookTeams.authenticate(test, conn, [])
222+
assert html_response(conn, 200) =~ "You don&#39;t have permission to access this server"
223+
end
224+
end
225+
61226
test "redirects to Livebook Teams with invalid access token",
62227
%{conn: conn, test: test} do
63228
conn = init_test_session(conn, %{livebook_teams_access_token: "1234567890"})
@@ -78,13 +243,12 @@ defmodule Livebook.ZTA.LivebookTeamsTest do
78243
conn = %Plug.Conn{conn | params: params_from_teams}
79244

80245
{conn, nil} = LivebookTeams.authenticate(test, conn, [])
81-
session = Plug.Conn.get_session(conn)
82-
83246
assert conn.status == 302
84247

85248
# Step 2: follow the redirect keeping the session set in previous request
86-
conn = build_conn(:get, redirected_to(conn))
87-
conn = init_test_session(conn, session)
249+
conn =
250+
build_conn(:get, redirected_to(conn))
251+
|> init_test_session(get_session(conn))
88252

89253
{conn, nil} = LivebookTeams.authenticate(test, conn, [])
90254

@@ -95,51 +259,44 @@ defmodule Livebook.ZTA.LivebookTeamsTest do
95259

96260
describe "logout/2" do
97261
test "revoke access token from Livebook Teams", %{conn: conn, node: node, test: test} do
98-
# Step 1: Get redirected to Livebook Teams
99-
conn = init_test_session(conn, %{})
100-
{conn, nil} = LivebookTeams.authenticate(test, conn, [])
101-
102-
[_, location] = Regex.run(~r/URL\("(.*?)"\)/, html_response(conn, 200))
103-
uri = URI.parse(location)
104-
assert uri.path == "/identity/authorize"
105-
assert %{"code" => code} = URI.decode_query(uri.query)
106-
107-
erpc_call(node, :allow_auth_request, [code])
108-
109-
# Step 2: Emulate the redirect back with the code for validation
110-
conn =
111-
build_conn(:get, "/", %{teams_identity: "", code: code})
112-
|> init_test_session(Plug.Conn.get_session(conn))
113-
114-
assert {conn, %{id: _id, name: _, email: _, payload: %{}} = metadata} =
115-
LivebookTeams.authenticate(test, conn, [])
116-
117-
assert redirected_to(conn, 302) == "/"
118-
119-
# Step 3: Confirm the token is valid for future requests
120-
conn =
121-
build_conn(:get, "/")
122-
|> init_test_session(Plug.Conn.get_session(conn))
123-
262+
{conn, _code, metadata} = authenticate_user(conn, node, test)
124263
assert {%{halted: false}, ^metadata} = LivebookTeams.authenticate(test, conn, [])
125264

126-
# Step 4: Revoke the token and the metadata will be invalid for future requests
127-
conn =
128-
build_conn(:get, "/")
129-
|> init_test_session(Plug.Conn.get_session(conn))
130-
265+
# Revoke the token and the metadata will be invalid for future requests
131266
assert %{status: 302} = conn = LivebookTeams.logout(test, conn)
132267
[url] = get_resp_header(conn, "location")
133268
assert %{status: 200} = Req.get!(url)
134269

135-
# Step 5: It we try to authenticate again, it should redirect to Teams
270+
# If we try to authenticate again, it should redirect to Teams
136271
conn =
137-
build_conn(:get, "/")
138-
|> init_test_session(Plug.Conn.get_session(conn))
272+
build_conn(:get, ~p"/")
273+
|> init_test_session(get_session(conn))
139274

140275
{conn, nil} = LivebookTeams.authenticate(test, conn, [])
141276
assert conn.halted
142277
assert html_response(conn, 200) =~ "window.location.href = "
143278
end
144279
end
280+
281+
defp authenticate_user(conn, node, test) do
282+
conn = init_test_session(conn, %{})
283+
{conn, nil} = LivebookTeams.authenticate(test, conn, [])
284+
285+
[_, location] = Regex.run(~r/URL\("(.*?)"\)/, html_response(conn, 200))
286+
uri = URI.parse(location)
287+
%{"code" => code} = URI.decode_query(uri.query)
288+
289+
erpc_call(node, :allow_auth_request, [code])
290+
291+
session = get_session(conn)
292+
293+
conn =
294+
build_conn(:get, ~p"/", %{teams_identity: "", code: code})
295+
|> init_test_session(session)
296+
297+
{conn, %{id: _id} = metadata} = LivebookTeams.authenticate(test, conn, [])
298+
session = get_session(conn)
299+
300+
{init_test_session(build_conn(), session), code, metadata}
301+
end
145302
end

0 commit comments

Comments
 (0)