Skip to content

Commit 9043edb

Browse files
authored
Implement authorization to deploy apps (#3044)
1 parent f0a1a3c commit 9043edb

File tree

14 files changed

+344
-27
lines changed

14 files changed

+344
-27
lines changed

lib/livebook/hubs/team_client.ex

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,14 @@ defmodule Livebook.Hubs.TeamClient do
164164
GenServer.call(registry_name(id), {:check_app_access, groups, slug})
165165
end
166166

167+
@doc """
168+
Returns if the given user has access to deploy apps to given deployment group.
169+
"""
170+
@spec user_can_deploy?(String.t(), pos_integer() | nil, String.t()) :: boolean()
171+
def user_can_deploy?(id, user_id, deployment_group_id) do
172+
GenServer.call(registry_name(id), {:user_can_deploy?, user_id, deployment_group_id})
173+
end
174+
167175
@doc """
168176
Returns if the Team client is connected.
169177
"""
@@ -338,6 +346,30 @@ defmodule Livebook.Hubs.TeamClient do
338346
end
339347
end
340348

349+
def handle_call({:user_can_deploy?, user_id, id}, _caller, state) do
350+
# App servers/Offline instances should not be able to deploy apps
351+
if state.deployment_group_id || user_id == nil do
352+
{:reply, false, state}
353+
else
354+
case fetch_deployment_group(id, state) do
355+
{:ok, deployment_group} ->
356+
deployment_user = %Teams.DeploymentUser{
357+
user_id: to_string(user_id),
358+
deployment_group_id: id
359+
}
360+
361+
authorized? =
362+
not deployment_group.deploy_auth or
363+
deployment_user in deployment_group.deployment_users
364+
365+
{:reply, authorized?, state}
366+
367+
_ ->
368+
{:reply, false, state}
369+
end
370+
end
371+
end
372+
341373
@impl true
342374
def handle_info(:connected, state) do
343375
Hubs.Broadcasts.hub_connected(state.hub.id)
@@ -499,6 +531,7 @@ defmodule Livebook.Hubs.TeamClient do
499531
agent_keys = Enum.map(deployment_group.agent_keys, &build_agent_key/1)
500532
environment_variables = build_environment_variables(state, deployment_group)
501533
authorization_groups = build_authorization_groups(deployment_group)
534+
deployment_users = build_deployment_users(deployment_group)
502535

503536
%Teams.DeploymentGroup{
504537
id: deployment_group.id,
@@ -512,7 +545,9 @@ defmodule Livebook.Hubs.TeamClient do
512545
url: nullify(deployment_group.url),
513546
teams_auth: deployment_group.teams_auth,
514547
groups_auth: deployment_group.groups_auth,
515-
authorization_groups: authorization_groups
548+
deploy_auth: deployment_group.deploy_auth,
549+
authorization_groups: authorization_groups,
550+
deployment_users: deployment_users
516551
}
517552
end
518553

@@ -530,7 +565,8 @@ defmodule Livebook.Hubs.TeamClient do
530565
clustering: nullify(deployment_group_created.clustering),
531566
url: nullify(deployment_group_created.url),
532567
teams_auth: deployment_group_created.teams_auth,
533-
authorization_groups: []
568+
authorization_groups: [],
569+
deployment_users: []
534570
}
535571
end
536572

@@ -539,6 +575,7 @@ defmodule Livebook.Hubs.TeamClient do
539575
agent_keys = Enum.map(deployment_group_updated.agent_keys, &build_agent_key/1)
540576
environment_variables = build_environment_variables(state, deployment_group_updated)
541577
authorization_groups = build_authorization_groups(deployment_group_updated)
578+
deployment_users = build_deployment_users(deployment_group_updated)
542579

543580
{:ok, deployment_group} = fetch_deployment_group(deployment_group_updated.id, state)
544581

@@ -552,7 +589,9 @@ defmodule Livebook.Hubs.TeamClient do
552589
url: nullify(deployment_group_updated.url),
553590
teams_auth: deployment_group_updated.teams_auth,
554591
groups_auth: deployment_group_updated.groups_auth,
555-
authorization_groups: authorization_groups
592+
deploy_auth: deployment_group_updated.deploy_auth,
593+
authorization_groups: authorization_groups,
594+
deployment_users: deployment_users
556595
}
557596
end
558597

@@ -596,6 +635,15 @@ defmodule Livebook.Hubs.TeamClient do
596635
end
597636
end
598637

638+
defp build_deployment_users(%{deployment_users: deployment_users}) do
639+
for deployment_user <- deployment_users do
640+
%Teams.DeploymentUser{
641+
user_id: deployment_user.user_id,
642+
deployment_group_id: deployment_user.deployment_group_id
643+
}
644+
end
645+
end
646+
599647
defp put_agent(state, agent) do
600648
state = remove_agent(state, agent)
601649

@@ -696,11 +744,19 @@ defmodule Livebook.Hubs.TeamClient do
696744

697745
with {:ok, current_deployment_group} <- fetch_deployment_group(deployment_group.id, state) do
698746
if state.deployment_group_id == deployment_group.id and
699-
(current_deployment_group.authorization_groups != deployment_group.authorization_groups or
747+
(current_deployment_group.authorization_groups !=
748+
deployment_group.authorization_groups or
700749
current_deployment_group.groups_auth != deployment_group.groups_auth or
701750
current_deployment_group.teams_auth != deployment_group.teams_auth) do
702751
Teams.Broadcasts.server_authorization_updated(deployment_group)
703752
end
753+
754+
if state.deployment_group_id == nil and
755+
(current_deployment_group.deployment_users !=
756+
deployment_group.deployment_users or
757+
current_deployment_group.deploy_auth != deployment_group.deploy_auth) do
758+
Teams.Broadcasts.deployment_users_updated(deployment_group)
759+
end
704760
end
705761

706762
put_deployment_group(state, deployment_group)

lib/livebook/teams.ex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,4 +294,12 @@ defmodule Livebook.Teams do
294294
defp add_external_errors(struct, errors_map) do
295295
struct |> Ecto.Changeset.change() |> add_external_errors(errors_map)
296296
end
297+
298+
@doc """
299+
Checks if the given user has access to deploy apps to given deployment group.
300+
"""
301+
@spec user_can_deploy?(Team.t(), Teams.DeploymentGroup.t()) :: boolean()
302+
def user_can_deploy?(%Team{} = team, %Teams.DeploymentGroup{} = deployment_group) do
303+
TeamClient.user_can_deploy?(team.id, team.user_id, deployment_group.id)
304+
end
297305
end

lib/livebook/teams/broadcasts.ex

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ defmodule Livebook.Teams.Broadcasts do
3434
* `{:deployment_group_created, DeploymentGroup.t()}`
3535
* `{:deployment_group_updated, DeploymentGroup.t()}`
3636
* `{:deployment_group_deleted, DeploymentGroup.t()}`
37+
* `{:deployment_users_updated, DeploymentGroup.t()}`
3738
3839
Topic `#{@app_server_topic}`:
3940
@@ -97,6 +98,14 @@ defmodule Livebook.Teams.Broadcasts do
9798
broadcast(@deployment_groups_topic, {:deployment_group_deleted, deployment_group})
9899
end
99100

101+
@doc """
102+
Broadcasts under `#{@deployment_groups_topic}` topic when hub received an updated deployment group that changed which users have access to deploy apps.
103+
"""
104+
@spec deployment_users_updated(Teams.DeploymentGroup.t()) :: broadcast()
105+
def deployment_users_updated(%Teams.DeploymentGroup{} = deployment_group) do
106+
broadcast(@deployment_groups_topic, {:deployment_users_updated, deployment_group})
107+
end
108+
100109
@doc """
101110
Broadcasts under `#{@app_deployments_topic}` topic when hub received to start a new app deployment.
102111
"""
@@ -138,7 +147,7 @@ defmodule Livebook.Teams.Broadcasts do
138147
end
139148

140149
@doc """
141-
Broadcasts under `#{@app_server_topic}` topic when hub received a updated deployment group that changed which groups have access to the server.
150+
Broadcasts under `#{@app_server_topic}` topic when hub received an updated deployment group that changed which groups have access to the server.
142151
"""
143152
@spec server_authorization_updated(Teams.DeploymentGroup.t()) :: broadcast()
144153
def server_authorization_updated(%Teams.DeploymentGroup{} = deployment_group) do

lib/livebook/teams/deployment_group.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ defmodule Livebook.Teams.DeploymentGroup do
1515
teams_auth: boolean(),
1616
groups_auth: boolean(),
1717
authorization_groups: Ecto.Schema.embeds_many(Teams.AuthorizationGroup.t()),
18+
deployment_users: Ecto.Schema.embeds_many(Teams.DeploymentUser.t()),
1819
secrets: Ecto.Schema.has_many(Secrets.Secret.t()),
1920
agent_keys: Ecto.Schema.has_many(Teams.AgentKey.t()),
2021
environment_variables: Ecto.Schema.has_many(Teams.EnvironmentVariable.t())
@@ -29,11 +30,13 @@ defmodule Livebook.Teams.DeploymentGroup do
2930
field :url, :string
3031
field :teams_auth, :boolean, default: true
3132
field :groups_auth, :boolean, default: false
33+
field :deploy_auth, :boolean, default: false
3234

3335
has_many :secrets, Secrets.Secret
3436
has_many :agent_keys, Teams.AgentKey
3537
has_many :environment_variables, Teams.EnvironmentVariable
3638
embeds_many :authorization_groups, Teams.AuthorizationGroup
39+
embeds_many :deployment_users, Teams.DeploymentUser
3740
end
3841

3942
def changeset(deployment_group, attrs \\ %{}) do

lib/livebook/teams/deployment_user.ex

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
defmodule Livebook.Teams.DeploymentUser do
2+
use Ecto.Schema
3+
4+
@type t :: %__MODULE__{
5+
user_id: String.t() | nil,
6+
deployment_group_id: String.t() | nil
7+
}
8+
9+
@primary_key false
10+
embedded_schema do
11+
field :user_id, :string
12+
field :deployment_group_id, :string
13+
end
14+
end

lib/livebook/teams/requests.ex

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ defmodule Livebook.Teams.Requests do
88
@deploy_key_prefix Teams.Constants.deploy_key_prefix()
99
@error_message "Something went wrong, try again later or please file a bug if it persists"
1010
@unauthorized_error_message "You are not authorized to perform this action, make sure you have the access and you are not in a Livebook App Server/Offline instance"
11+
@unauthorized_app_deployment_error_message "You are not authorized to perform this action, make sure you have the access to deploy apps to this deployment group"
1112

1213
@typep api_result :: {:ok, map()} | error_result()
1314
@typep error_result :: {:error, map() | String.t()} | {:transport_error, String.t()}
@@ -300,6 +301,11 @@ defmodule Livebook.Teams.Requests do
300301
defp upload(path, content, params, team) do
301302
build_req(team)
302303
|> Req.Request.put_header("content-length", "#{byte_size(content)}")
304+
|> Req.Request.append_response_steps(
305+
livebook_put_private: fn {request, response} ->
306+
{request, Req.Response.put_private(response, :livebook_app_deployment, true)}
307+
end
308+
)
303309
|> Req.post(url: path, params: params, body: content)
304310
|> handle_response()
305311
|> dispatch_messages(team)
@@ -337,10 +343,20 @@ defmodule Livebook.Teams.Requests do
337343

338344
defp handle_response(response) do
339345
case response do
340-
{:ok, %{status: status} = response} when status in 200..299 -> {:ok, response.body}
341-
{:ok, %{status: status} = response} when status in [410, 422] -> return_error(response)
342-
{:ok, %{status: 401}} -> {:transport_error, @unauthorized_error_message}
343-
_otherwise -> {:transport_error, @error_message}
346+
{:ok, %{status: status} = response} when status in 200..299 ->
347+
{:ok, response.body}
348+
349+
{:ok, %{status: status} = response} when status in [410, 422] ->
350+
return_error(response)
351+
352+
{:ok, %{status: 401, private: %{livebook_app_deployment: true}}} ->
353+
{:transport_error, @unauthorized_app_deployment_error_message}
354+
355+
{:ok, %{status: 401}} ->
356+
{:transport_error, @unauthorized_error_message}
357+
358+
_otherwise ->
359+
{:transport_error, @error_message}
344360
end
345361
end
346362

0 commit comments

Comments
 (0)