Skip to content

Commit 73c0f1b

Browse files
authored
Handle authorization groups in real-time (#2998)
1 parent 7fa44a3 commit 73c0f1b

24 files changed

Lines changed: 948 additions & 89 deletions

lib/livebook/apps.ex

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ defmodule Livebook.Apps do
77
require Logger
88

99
alias Livebook.App
10+
alias Livebook.Apps
1011

1112
@doc """
1213
Returns app process pid for the given slug.
@@ -79,13 +80,10 @@ defmodule Livebook.Apps do
7980
@spec authorized?(App.t(), Livebook.Users.User.t()) :: boolean()
8081
def authorized?(app, user)
8182

82-
def authorized?(%{app_spec: %Livebook.Apps.TeamsAppSpec{}}, %{restricted_apps_groups: []}),
83-
do: false
83+
def authorized?(_app, %{access_type: :full}), do: true
8484

85-
def authorized?(_app, %{restricted_apps_groups: nil}), do: true
86-
87-
def authorized?(%{slug: slug, app_spec: %Livebook.Apps.TeamsAppSpec{hub_id: id}}, user) do
88-
Livebook.Hubs.TeamClient.user_app_access?(id, user.restricted_apps_groups, slug)
85+
def authorized?(%{slug: slug, app_spec: %Apps.TeamsAppSpec{hub_id: id}}, user) do
86+
Livebook.Hubs.TeamClient.user_app_access?(id, user.groups, slug)
8987
end
9088

9189
@doc """

lib/livebook/hubs/team_client.ex

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -296,26 +296,39 @@ defmodule Livebook.Hubs.TeamClient do
296296
end
297297
end
298298

299-
def handle_call({:check_full_access, groups}, _caller, %{deployment_group_id: id} = state) do
300-
case fetch_deployment_group(id, state) do
301-
{:ok, deployment_group} ->
302-
{:reply, authorized_group?(deployment_group.authorization_groups, groups), state}
303-
304-
_ ->
305-
{:reply, false, state}
299+
def handle_call({:check_full_access, groups}, _caller, state) do
300+
if id = state.deployment_group_id do
301+
case fetch_deployment_group(id, state) do
302+
{:ok, deployment_group} ->
303+
{:reply,
304+
not deployment_group.teams_auth or
305+
not deployment_group.groups_auth or
306+
authorized_group?(deployment_group.authorization_groups, groups), state}
307+
308+
_ ->
309+
{:reply, false, state}
310+
end
311+
else
312+
{:reply, true, state}
306313
end
307314
end
308315

309-
def handle_call({:check_app_access, groups, slug}, _caller, %{deployment_group_id: id} = state) do
310-
with {:ok, deployment_group} <- fetch_deployment_group(id, state),
311-
{:ok, app_deployment} <- fetch_app_deployment_from_slug(slug, state) do
312-
app_access? =
313-
authorized_group?(deployment_group.authorization_groups, groups) or
314-
authorized_group?(app_deployment.authorization_groups, groups)
316+
def handle_call({:check_app_access, groups, slug}, _caller, state) do
317+
if id = state.deployment_group_id do
318+
with {:ok, deployment_group} <- fetch_deployment_group(id, state),
319+
{:ok, app_deployment} <- fetch_app_deployment_from_slug(slug, state) do
320+
app_access? =
321+
not deployment_group.teams_auth or
322+
not deployment_group.groups_auth or
323+
(authorized_group?(deployment_group.authorization_groups, groups) or
324+
authorized_group?(app_deployment.authorization_groups, groups))
315325

316-
{:reply, app_access?, state}
326+
{:reply, app_access?, state}
327+
else
328+
_ -> {:reply, false, state}
329+
end
317330
else
318-
_ -> {:reply, false, state}
331+
{:reply, true, state}
319332
end
320333
end
321334

@@ -492,6 +505,7 @@ defmodule Livebook.Hubs.TeamClient do
492505
clustering: nullify(deployment_group.clustering),
493506
url: nullify(deployment_group.url),
494507
teams_auth: deployment_group.teams_auth,
508+
groups_auth: deployment_group.groups_auth,
495509
authorization_groups: authorization_groups
496510
}
497511
end
@@ -531,6 +545,7 @@ defmodule Livebook.Hubs.TeamClient do
531545
clustering: atomize(deployment_group_updated.clustering),
532546
url: nullify(deployment_group_updated.url),
533547
teams_auth: deployment_group_updated.teams_auth,
548+
groups_auth: deployment_group_updated.groups_auth,
534549
authorization_groups: authorization_groups
535550
}
536551
end
@@ -659,7 +674,6 @@ defmodule Livebook.Hubs.TeamClient do
659674

660675
defp handle_event(:deployment_group_created, %Teams.DeploymentGroup{} = deployment_group, state) do
661676
Teams.Broadcasts.deployment_group_created(deployment_group)
662-
663677
put_deployment_group(state, deployment_group)
664678
end
665679

@@ -673,6 +687,16 @@ defmodule Livebook.Hubs.TeamClient do
673687

674688
defp handle_event(:deployment_group_updated, %Teams.DeploymentGroup{} = deployment_group, state) do
675689
Teams.Broadcasts.deployment_group_updated(deployment_group)
690+
691+
with {:ok, current_deployment_group} <- fetch_deployment_group(deployment_group.id, state) do
692+
if state.deployment_group_id == deployment_group.id and
693+
(current_deployment_group.authorization_groups != deployment_group.authorization_groups or
694+
current_deployment_group.groups_auth != deployment_group.groups_auth or
695+
current_deployment_group.teams_auth != deployment_group.teams_auth) do
696+
Teams.Broadcasts.server_authorization_updated(deployment_group)
697+
end
698+
end
699+
676700
put_deployment_group(state, deployment_group)
677701
end
678702

lib/livebook/teams/broadcasts.ex

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ defmodule Livebook.Teams.Broadcasts do
77
@app_deployments_topic "teams:app_deployments"
88
@clients_topic "teams:clients"
99
@deployment_groups_topic "teams:deployment_groups"
10+
@app_server_topic "teams:app_server"
1011

1112
@doc """
1213
Subscribes to one or more subtopics in `"teams"`.
@@ -31,9 +32,13 @@ defmodule Livebook.Teams.Broadcasts do
3132
Topic `#{@deployment_groups_topic}`:
3233
3334
* `{:deployment_group_created, DeploymentGroup.t()}`
34-
* `{:deployment_group_update, DeploymentGroup.t()}`
35+
* `{:deployment_group_updated, DeploymentGroup.t()}`
3536
* `{:deployment_group_deleted, DeploymentGroup.t()}`
3637
38+
Topic `#{@app_server_topic}`:
39+
40+
* `{:server_authorization_updated, DeploymentGroup.t()}`
41+
3742
"""
3843
@spec subscribe(atom() | list(atom())) :: :ok | {:error, term()}
3944
def subscribe(topics) when is_list(topics) do
@@ -132,6 +137,14 @@ defmodule Livebook.Teams.Broadcasts do
132137
broadcast(@agents_topic, {:agent_left, agent})
133138
end
134139

140+
@doc """
141+
Broadcasts under `#{@app_server_topic}` topic when hub received a updated deployment group that changed which groups have access to the server.
142+
"""
143+
@spec server_authorization_updated(Teams.DeploymentGroup.t()) :: broadcast()
144+
def server_authorization_updated(%Teams.DeploymentGroup{} = deployment_group) do
145+
broadcast(@app_server_topic, {:server_authorization_updated, deployment_group})
146+
end
147+
135148
defp broadcast(topic, message) do
136149
Phoenix.PubSub.broadcast(Livebook.PubSub, topic, message)
137150
end

lib/livebook/teams/deployment_group.ex

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ defmodule Livebook.Teams.DeploymentGroup do
1313
clustering: :auto | :dns | nil,
1414
hub_id: String.t() | nil,
1515
teams_auth: boolean(),
16+
groups_auth: boolean(),
1617
authorization_groups: Ecto.Schema.embeds_many(Teams.AuthorizationGroup.t()),
1718
secrets: Ecto.Schema.has_many(Secrets.Secret.t()),
1819
agent_keys: Ecto.Schema.has_many(Teams.AgentKey.t()),
@@ -27,6 +28,7 @@ defmodule Livebook.Teams.DeploymentGroup do
2728
field :clustering, Ecto.Enum, values: [:auto, :dns]
2829
field :url, :string
2930
field :teams_auth, :boolean, default: true
31+
field :groups_auth, :boolean, default: false
3032

3133
has_many :secrets, Secrets.Secret
3234
has_many :agent_keys, Teams.AgentKey
@@ -36,7 +38,7 @@ defmodule Livebook.Teams.DeploymentGroup do
3638

3739
def changeset(deployment_group, attrs \\ %{}) do
3840
deployment_group
39-
|> cast(attrs, [:id, :name, :mode, :hub_id, :clustering, :url, :teams_auth])
41+
|> cast(attrs, [:id, :name, :mode, :hub_id, :clustering, :url])
4042
|> validate_required([:name, :mode])
4143
|> update_change(:url, fn url ->
4244
if url do

lib/livebook/teams/requests.ex

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,7 @@ defmodule Livebook.Teams.Requests do
172172
name: deployment_group.name,
173173
mode: deployment_group.mode,
174174
clustering: deployment_group.clustering,
175-
url: deployment_group.url,
176-
teams_auth: deployment_group.teams_auth
175+
url: deployment_group.url
177176
}
178177

179178
post("/api/v1/org/deployment-groups", params, team)

lib/livebook/users/user.ex

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@ defmodule Livebook.Users.User do
1212

1313
alias Livebook.Utils
1414

15+
@type access_type :: :full | :apps
16+
1517
@type t :: %__MODULE__{
1618
id: id(),
1719
name: String.t() | nil,
1820
email: String.t() | nil,
1921
avatar_url: String.t() | nil,
20-
restricted_apps_groups: list(map()) | nil,
22+
access_type: access_type(),
23+
groups: list(map()) | nil,
2124
payload: map() | nil,
2225
hex_color: hex_color()
2326
}
@@ -29,7 +32,8 @@ defmodule Livebook.Users.User do
2932
field :name, :string
3033
field :email, :string
3134
field :avatar_url, :string
32-
field :restricted_apps_groups, {:array, :map}
35+
field :access_type, Ecto.Enum, values: ~w[full apps]a, default: :full
36+
field :groups, {:array, :map}, default: []
3337
field :payload, :map
3438
field :hex_color, Livebook.EctoTypes.HexColor
3539
end
@@ -44,15 +48,17 @@ defmodule Livebook.Users.User do
4448
name: nil,
4549
email: nil,
4650
avatar_url: nil,
47-
restricted_apps_groups: nil,
51+
access_type: :full,
52+
groups: [],
4853
payload: nil,
4954
hex_color: Livebook.EctoTypes.HexColor.random()
5055
}
5156
end
5257

58+
@doc false
5359
def changeset(user, attrs \\ %{}) do
5460
user
55-
|> cast(attrs, [:name, :email, :avatar_url, :restricted_apps_groups, :hex_color, :payload])
61+
|> cast(attrs, [:name, :email, :avatar_url, :access_type, :groups, :hex_color, :payload])
5662
|> validate_required([:hex_color])
5763
end
5864
end

lib/livebook/zta.ex

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ defmodule Livebook.ZTA do
5858
* `:id` - a string that uniquely identifies the user
5959
* `:name` - the user name
6060
* `:email` - the user email
61+
* `:avatar_url` - the user avatar
62+
* `:access_type` - the user access type
63+
* `:groups` - the user groups
6164
* `:payload` - the provider payload
6265
6366
Note that none of the keys are required. The metadata returned depends
@@ -67,6 +70,9 @@ defmodule Livebook.ZTA do
6770
optional(:id) => String.t(),
6871
optional(:name) => String.t(),
6972
optional(:email) => String.t(),
73+
optional(:avatar_url) => String.t() | nil,
74+
optional(:access_type) => Livebook.Users.User.access_type(),
75+
optional(:groups) => list(map()),
7076
optional(:payload) => map()
7177
}
7278

lib/livebook/zta/livebook_teams.ex

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -159,31 +159,36 @@ defmodule Livebook.ZTA.LivebookTeams do
159159

160160
defp get_user_info(team, access_token) do
161161
with {:ok, payload} <- Teams.Requests.get_user_info(team, access_token) do
162-
%{
163-
"id" => id,
164-
"name" => name,
165-
"email" => email,
166-
"groups" => groups,
167-
"avatar_url" => avatar_url
168-
} = payload
169-
170-
restricted_apps_groups =
171-
if Livebook.Hubs.TeamClient.user_full_access?(team.id, groups) do
172-
nil
173-
else
174-
groups
175-
end
176-
177-
metadata = %{
178-
id: id,
179-
name: name,
180-
avatar_url: avatar_url,
181-
restricted_apps_groups: restricted_apps_groups,
182-
email: email,
183-
payload: payload
184-
}
185-
186-
{:ok, metadata}
162+
{:ok, build_metadata(team.id, payload)}
187163
end
188164
end
165+
166+
@doc """
167+
Returns the user metadata from given payload.
168+
"""
169+
@spec build_metadata(String.t(), map()) :: Livebook.ZTA.metadata()
170+
def build_metadata(hub_id, payload) do
171+
%{
172+
"id" => id,
173+
"name" => name,
174+
"email" => email,
175+
"groups" => groups,
176+
"avatar_url" => avatar_url
177+
} = payload
178+
179+
access_type =
180+
if Livebook.Hubs.TeamClient.user_full_access?(hub_id, groups),
181+
do: :full,
182+
else: :apps
183+
184+
%{
185+
id: id,
186+
name: name,
187+
avatar_url: avatar_url,
188+
access_type: access_type,
189+
groups: groups,
190+
email: email,
191+
payload: payload
192+
}
193+
end
189194
end

lib/livebook_web/live/app_live.ex

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,13 @@ defmodule LivebookWeb.AppLive do
1212
end
1313
end
1414

15-
def mount(_params, _session, socket) when not socket.assigns.app_authorized? do
16-
{:ok, socket, layout: false}
15+
def mount(%{"slug" => slug}, _session, socket) when not socket.assigns.app_authorized? do
16+
if connected?(socket) do
17+
Livebook.Teams.Broadcasts.subscribe(:app_deployments)
18+
end
19+
20+
{:ok, app} = Livebook.Apps.fetch_app(slug)
21+
{:ok, assign(socket, app: app), layout: false}
1722
end
1823

1924
def mount(%{"slug" => slug}, _session, socket) do
@@ -22,6 +27,7 @@ defmodule LivebookWeb.AppLive do
2227

2328
if connected?(socket) do
2429
Livebook.App.subscribe(slug)
30+
Livebook.Teams.Broadcasts.subscribe(:app_deployments)
2531
end
2632

2733
{:ok, assign(socket, app: app)}
@@ -122,6 +128,18 @@ defmodule LivebookWeb.AppLive do
122128
{:noreply, assign(socket, :app, app)}
123129
end
124130

131+
def handle_info(
132+
{:app_deployment_updated, %{slug: slug}},
133+
%{assigns: %{app: %{slug: slug} = app}} = socket
134+
) do
135+
if socket.assigns.app_authorized? and
136+
Livebook.Apps.authorized?(app, socket.assigns.current_user) do
137+
{:noreply, socket}
138+
else
139+
{:noreply, redirect(socket, to: ~p"/apps/#{slug}")}
140+
end
141+
end
142+
125143
def handle_info(_message, socket), do: {:noreply, socket}
126144

127145
defp active_sessions(sessions) do

lib/livebook_web/live/app_session_live.ex

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,13 @@ defmodule LivebookWeb.AppSessionLive do
2424
end
2525
end
2626

27-
def mount(_params, _session, socket) when not socket.assigns.app_authorized? do
28-
{:ok, socket, layout: false}
27+
def mount(%{"slug" => slug, "id" => session_id}, _session, socket)
28+
when not socket.assigns.app_authorized? do
29+
if connected?(socket) do
30+
Livebook.Teams.Broadcasts.subscribe(:app_deployments)
31+
end
32+
33+
{:ok, assign(socket, slug: slug, session: %{id: session_id}), layout: false}
2934
end
3035

3136
def mount(%{"slug" => slug, "id" => session_id}, _session, socket) do
@@ -388,7 +393,8 @@ defmodule LivebookWeb.AppSessionLive do
388393
# should't have access.
389394
{:ok, app} = Livebook.Apps.fetch_app(slug)
390395

391-
if Livebook.Apps.authorized?(app, socket.assigns.current_user) do
396+
if socket.assigns.app_authorized? and
397+
Livebook.Apps.authorized?(app, socket.assigns.current_user) do
392398
{:noreply, socket}
393399
else
394400
{:noreply, redirect(socket, to: ~p"/apps/#{slug}/sessions/#{socket.assigns.session.id}")}

0 commit comments

Comments
 (0)