Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 60 additions & 4 deletions lib/livebook/hubs/team_client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,14 @@ defmodule Livebook.Hubs.TeamClient do
GenServer.call(registry_name(id), {:check_app_access, groups, slug})
end

@doc """
Returns if the given user has access to deploy apps to given deployment group.
"""
@spec user_can_deploy?(String.t(), pos_integer() | nil, String.t()) :: boolean()
def user_can_deploy?(id, user_id, deployment_group_id) do
GenServer.call(registry_name(id), {:user_can_deploy?, user_id, deployment_group_id})
end

@doc """
Returns if the Team client is connected.
"""
Expand Down Expand Up @@ -338,6 +346,30 @@ defmodule Livebook.Hubs.TeamClient do
end
end

def handle_call({:user_can_deploy?, user_id, id}, _caller, state) do
# App servers/Offline instances should not be able to deploy apps
if state.deployment_group_id || user_id == nil do
{:reply, false, state}
else
case fetch_deployment_group(id, state) do
{:ok, deployment_group} ->
deployment_user = %Teams.DeploymentUser{
user_id: to_string(user_id),
deployment_group_id: id
}

authorized? =
not deployment_group.deploy_auth or
deployment_user in deployment_group.deployment_users

{:reply, authorized?, state}

_ ->
{:reply, false, state}
end
end
end

@impl true
def handle_info(:connected, state) do
Hubs.Broadcasts.hub_connected(state.hub.id)
Expand Down Expand Up @@ -499,6 +531,7 @@ defmodule Livebook.Hubs.TeamClient do
agent_keys = Enum.map(deployment_group.agent_keys, &build_agent_key/1)
environment_variables = build_environment_variables(state, deployment_group)
authorization_groups = build_authorization_groups(deployment_group)
deployment_users = build_deployment_users(deployment_group)

%Teams.DeploymentGroup{
id: deployment_group.id,
Expand All @@ -512,7 +545,9 @@ defmodule Livebook.Hubs.TeamClient do
url: nullify(deployment_group.url),
teams_auth: deployment_group.teams_auth,
groups_auth: deployment_group.groups_auth,
authorization_groups: authorization_groups
deploy_auth: deployment_group.deploy_auth,
authorization_groups: authorization_groups,
deployment_users: deployment_users
}
end

Expand All @@ -530,7 +565,8 @@ defmodule Livebook.Hubs.TeamClient do
clustering: nullify(deployment_group_created.clustering),
url: nullify(deployment_group_created.url),
teams_auth: deployment_group_created.teams_auth,
authorization_groups: []
authorization_groups: [],
deployment_users: []
}
end

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

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

Expand All @@ -552,7 +589,9 @@ defmodule Livebook.Hubs.TeamClient do
url: nullify(deployment_group_updated.url),
teams_auth: deployment_group_updated.teams_auth,
groups_auth: deployment_group_updated.groups_auth,
authorization_groups: authorization_groups
deploy_auth: deployment_group_updated.deploy_auth,
authorization_groups: authorization_groups,
deployment_users: deployment_users
}
end

Expand Down Expand Up @@ -596,6 +635,15 @@ defmodule Livebook.Hubs.TeamClient do
end
end

defp build_deployment_users(%{deployment_users: deployment_users}) do
for deployment_user <- deployment_users do
%Teams.DeploymentUser{
user_id: deployment_user.user_id,
deployment_group_id: deployment_user.deployment_group_id
}
end
end

defp put_agent(state, agent) do
state = remove_agent(state, agent)

Expand Down Expand Up @@ -696,11 +744,19 @@ defmodule Livebook.Hubs.TeamClient do

with {:ok, current_deployment_group} <- fetch_deployment_group(deployment_group.id, state) do
if state.deployment_group_id == deployment_group.id and
(current_deployment_group.authorization_groups != deployment_group.authorization_groups or
(current_deployment_group.authorization_groups !=
deployment_group.authorization_groups or
current_deployment_group.groups_auth != deployment_group.groups_auth or
current_deployment_group.teams_auth != deployment_group.teams_auth) do
Teams.Broadcasts.server_authorization_updated(deployment_group)
end

if state.deployment_group_id == nil and
(current_deployment_group.deployment_users !=
deployment_group.deployment_users or
current_deployment_group.deploy_auth != deployment_group.deploy_auth) do
Teams.Broadcasts.deployment_users_updated(deployment_group)
end
end

put_deployment_group(state, deployment_group)
Expand Down
8 changes: 8 additions & 0 deletions lib/livebook/teams.ex
Original file line number Diff line number Diff line change
Expand Up @@ -294,4 +294,12 @@ defmodule Livebook.Teams do
defp add_external_errors(struct, errors_map) do
struct |> Ecto.Changeset.change() |> add_external_errors(errors_map)
end

@doc """
Checks if the given user has access to deploy apps to given deployment group.
"""
@spec user_can_deploy?(Team.t(), Teams.DeploymentGroup.t()) :: boolean()
def user_can_deploy?(%Team{} = team, %Teams.DeploymentGroup{} = deployment_group) do
TeamClient.user_can_deploy?(team.id, team.user_id, deployment_group.id)
end
end
11 changes: 10 additions & 1 deletion lib/livebook/teams/broadcasts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ defmodule Livebook.Teams.Broadcasts do
* `{:deployment_group_created, DeploymentGroup.t()}`
* `{:deployment_group_updated, DeploymentGroup.t()}`
* `{:deployment_group_deleted, DeploymentGroup.t()}`
* `{:deployment_users_updated, DeploymentGroup.t()}`

Topic `#{@app_server_topic}`:

Expand Down Expand Up @@ -97,6 +98,14 @@ defmodule Livebook.Teams.Broadcasts do
broadcast(@deployment_groups_topic, {:deployment_group_deleted, deployment_group})
end

@doc """
Broadcasts under `#{@deployment_groups_topic}` topic when hub received an updated deployment group that changed which users have access to deploy apps.
"""
@spec deployment_users_updated(Teams.DeploymentGroup.t()) :: broadcast()
def deployment_users_updated(%Teams.DeploymentGroup{} = deployment_group) do
broadcast(@deployment_groups_topic, {:deployment_users_updated, deployment_group})
end

@doc """
Broadcasts under `#{@app_deployments_topic}` topic when hub received to start a new app deployment.
"""
Expand Down Expand Up @@ -138,7 +147,7 @@ defmodule Livebook.Teams.Broadcasts do
end

@doc """
Broadcasts under `#{@app_server_topic}` topic when hub received a updated deployment group that changed which groups have access to the server.
Broadcasts under `#{@app_server_topic}` topic when hub received an updated deployment group that changed which groups have access to the server.
"""
@spec server_authorization_updated(Teams.DeploymentGroup.t()) :: broadcast()
def server_authorization_updated(%Teams.DeploymentGroup{} = deployment_group) do
Expand Down
3 changes: 3 additions & 0 deletions lib/livebook/teams/deployment_group.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ defmodule Livebook.Teams.DeploymentGroup do
teams_auth: boolean(),
groups_auth: boolean(),
authorization_groups: Ecto.Schema.embeds_many(Teams.AuthorizationGroup.t()),
deployment_users: Ecto.Schema.embeds_many(Teams.DeploymentUser.t()),
secrets: Ecto.Schema.has_many(Secrets.Secret.t()),
agent_keys: Ecto.Schema.has_many(Teams.AgentKey.t()),
environment_variables: Ecto.Schema.has_many(Teams.EnvironmentVariable.t())
Expand All @@ -29,11 +30,13 @@ defmodule Livebook.Teams.DeploymentGroup do
field :url, :string
field :teams_auth, :boolean, default: true
field :groups_auth, :boolean, default: false
field :deploy_auth, :boolean, default: false

has_many :secrets, Secrets.Secret
has_many :agent_keys, Teams.AgentKey
has_many :environment_variables, Teams.EnvironmentVariable
embeds_many :authorization_groups, Teams.AuthorizationGroup
embeds_many :deployment_users, Teams.DeploymentUser
end

def changeset(deployment_group, attrs \\ %{}) do
Expand Down
14 changes: 14 additions & 0 deletions lib/livebook/teams/deployment_user.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
defmodule Livebook.Teams.DeploymentUser do
use Ecto.Schema

@type t :: %__MODULE__{
user_id: String.t() | nil,
deployment_group_id: String.t() | nil
}

@primary_key false
embedded_schema do
field :user_id, :string
field :deployment_group_id, :string
end
end
24 changes: 20 additions & 4 deletions lib/livebook/teams/requests.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ defmodule Livebook.Teams.Requests do
@deploy_key_prefix Teams.Constants.deploy_key_prefix()
@error_message "Something went wrong, try again later or please file a bug if it persists"
@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"
@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"

@typep api_result :: {:ok, map()} | error_result()
@typep error_result :: {:error, map() | String.t()} | {:transport_error, String.t()}
Expand Down Expand Up @@ -300,6 +301,11 @@ defmodule Livebook.Teams.Requests do
defp upload(path, content, params, team) do
build_req(team)
|> Req.Request.put_header("content-length", "#{byte_size(content)}")
|> Req.Request.append_response_steps(
livebook_put_private: fn {request, response} ->
{request, Req.Response.put_private(response, :livebook_app_deployment, true)}
end
)
|> Req.post(url: path, params: params, body: content)
|> handle_response()
|> dispatch_messages(team)
Expand Down Expand Up @@ -337,10 +343,20 @@ defmodule Livebook.Teams.Requests do

defp handle_response(response) do
case response do
{:ok, %{status: status} = response} when status in 200..299 -> {:ok, response.body}
{:ok, %{status: status} = response} when status in [410, 422] -> return_error(response)
{:ok, %{status: 401}} -> {:transport_error, @unauthorized_error_message}
_otherwise -> {:transport_error, @error_message}
{:ok, %{status: status} = response} when status in 200..299 ->
{:ok, response.body}

{:ok, %{status: status} = response} when status in [410, 422] ->
return_error(response)

{:ok, %{status: 401, private: %{livebook_app_deployment: true}}} ->
{:transport_error, @unauthorized_app_deployment_error_message}

{:ok, %{status: 401}} ->
{:transport_error, @unauthorized_error_message}

_otherwise ->
{:transport_error, @error_message}
end
end

Expand Down
Loading