mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-04 20:14:57 +08:00
Implement authorization to deploy apps (#3044)
This commit is contained in:
parent
f0a1a3cfac
commit
9043edb748
14 changed files with 344 additions and 27 deletions
|
@ -164,6 +164,14 @@ defmodule Livebook.Hubs.TeamClient do
|
||||||
GenServer.call(registry_name(id), {:check_app_access, groups, slug})
|
GenServer.call(registry_name(id), {:check_app_access, groups, slug})
|
||||||
end
|
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 """
|
@doc """
|
||||||
Returns if the Team client is connected.
|
Returns if the Team client is connected.
|
||||||
"""
|
"""
|
||||||
|
@ -338,6 +346,30 @@ defmodule Livebook.Hubs.TeamClient do
|
||||||
end
|
end
|
||||||
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
|
@impl true
|
||||||
def handle_info(:connected, state) do
|
def handle_info(:connected, state) do
|
||||||
Hubs.Broadcasts.hub_connected(state.hub.id)
|
Hubs.Broadcasts.hub_connected(state.hub.id)
|
||||||
|
@ -499,6 +531,7 @@ defmodule Livebook.Hubs.TeamClient do
|
||||||
agent_keys = Enum.map(deployment_group.agent_keys, &build_agent_key/1)
|
agent_keys = Enum.map(deployment_group.agent_keys, &build_agent_key/1)
|
||||||
environment_variables = build_environment_variables(state, deployment_group)
|
environment_variables = build_environment_variables(state, deployment_group)
|
||||||
authorization_groups = build_authorization_groups(deployment_group)
|
authorization_groups = build_authorization_groups(deployment_group)
|
||||||
|
deployment_users = build_deployment_users(deployment_group)
|
||||||
|
|
||||||
%Teams.DeploymentGroup{
|
%Teams.DeploymentGroup{
|
||||||
id: deployment_group.id,
|
id: deployment_group.id,
|
||||||
|
@ -512,7 +545,9 @@ defmodule Livebook.Hubs.TeamClient do
|
||||||
url: nullify(deployment_group.url),
|
url: nullify(deployment_group.url),
|
||||||
teams_auth: deployment_group.teams_auth,
|
teams_auth: deployment_group.teams_auth,
|
||||||
groups_auth: deployment_group.groups_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
|
end
|
||||||
|
|
||||||
|
@ -530,7 +565,8 @@ defmodule Livebook.Hubs.TeamClient do
|
||||||
clustering: nullify(deployment_group_created.clustering),
|
clustering: nullify(deployment_group_created.clustering),
|
||||||
url: nullify(deployment_group_created.url),
|
url: nullify(deployment_group_created.url),
|
||||||
teams_auth: deployment_group_created.teams_auth,
|
teams_auth: deployment_group_created.teams_auth,
|
||||||
authorization_groups: []
|
authorization_groups: [],
|
||||||
|
deployment_users: []
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -539,6 +575,7 @@ defmodule Livebook.Hubs.TeamClient do
|
||||||
agent_keys = Enum.map(deployment_group_updated.agent_keys, &build_agent_key/1)
|
agent_keys = Enum.map(deployment_group_updated.agent_keys, &build_agent_key/1)
|
||||||
environment_variables = build_environment_variables(state, deployment_group_updated)
|
environment_variables = build_environment_variables(state, deployment_group_updated)
|
||||||
authorization_groups = build_authorization_groups(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)
|
{:ok, deployment_group} = fetch_deployment_group(deployment_group_updated.id, state)
|
||||||
|
|
||||||
|
@ -552,7 +589,9 @@ defmodule Livebook.Hubs.TeamClient do
|
||||||
url: nullify(deployment_group_updated.url),
|
url: nullify(deployment_group_updated.url),
|
||||||
teams_auth: deployment_group_updated.teams_auth,
|
teams_auth: deployment_group_updated.teams_auth,
|
||||||
groups_auth: deployment_group_updated.groups_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
|
end
|
||||||
|
|
||||||
|
@ -596,6 +635,15 @@ defmodule Livebook.Hubs.TeamClient do
|
||||||
end
|
end
|
||||||
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
|
defp put_agent(state, agent) do
|
||||||
state = remove_agent(state, agent)
|
state = remove_agent(state, agent)
|
||||||
|
|
||||||
|
@ -696,11 +744,19 @@ defmodule Livebook.Hubs.TeamClient do
|
||||||
|
|
||||||
with {:ok, current_deployment_group} <- fetch_deployment_group(deployment_group.id, state) do
|
with {:ok, current_deployment_group} <- fetch_deployment_group(deployment_group.id, state) do
|
||||||
if state.deployment_group_id == deployment_group.id and
|
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.groups_auth != deployment_group.groups_auth or
|
||||||
current_deployment_group.teams_auth != deployment_group.teams_auth) do
|
current_deployment_group.teams_auth != deployment_group.teams_auth) do
|
||||||
Teams.Broadcasts.server_authorization_updated(deployment_group)
|
Teams.Broadcasts.server_authorization_updated(deployment_group)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
put_deployment_group(state, deployment_group)
|
put_deployment_group(state, deployment_group)
|
||||||
|
|
|
@ -294,4 +294,12 @@ defmodule Livebook.Teams do
|
||||||
defp add_external_errors(struct, errors_map) do
|
defp add_external_errors(struct, errors_map) do
|
||||||
struct |> Ecto.Changeset.change() |> add_external_errors(errors_map)
|
struct |> Ecto.Changeset.change() |> add_external_errors(errors_map)
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -34,6 +34,7 @@ defmodule Livebook.Teams.Broadcasts do
|
||||||
* `{:deployment_group_created, DeploymentGroup.t()}`
|
* `{:deployment_group_created, DeploymentGroup.t()}`
|
||||||
* `{:deployment_group_updated, DeploymentGroup.t()}`
|
* `{:deployment_group_updated, DeploymentGroup.t()}`
|
||||||
* `{:deployment_group_deleted, DeploymentGroup.t()}`
|
* `{:deployment_group_deleted, DeploymentGroup.t()}`
|
||||||
|
* `{:deployment_users_updated, DeploymentGroup.t()}`
|
||||||
|
|
||||||
Topic `#{@app_server_topic}`:
|
Topic `#{@app_server_topic}`:
|
||||||
|
|
||||||
|
@ -97,6 +98,14 @@ defmodule Livebook.Teams.Broadcasts do
|
||||||
broadcast(@deployment_groups_topic, {:deployment_group_deleted, deployment_group})
|
broadcast(@deployment_groups_topic, {:deployment_group_deleted, deployment_group})
|
||||||
end
|
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 """
|
@doc """
|
||||||
Broadcasts under `#{@app_deployments_topic}` topic when hub received to start a new app deployment.
|
Broadcasts under `#{@app_deployments_topic}` topic when hub received to start a new app deployment.
|
||||||
"""
|
"""
|
||||||
|
@ -138,7 +147,7 @@ defmodule Livebook.Teams.Broadcasts do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@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()
|
@spec server_authorization_updated(Teams.DeploymentGroup.t()) :: broadcast()
|
||||||
def server_authorization_updated(%Teams.DeploymentGroup{} = deployment_group) do
|
def server_authorization_updated(%Teams.DeploymentGroup{} = deployment_group) do
|
||||||
|
|
|
@ -15,6 +15,7 @@ defmodule Livebook.Teams.DeploymentGroup do
|
||||||
teams_auth: boolean(),
|
teams_auth: boolean(),
|
||||||
groups_auth: boolean(),
|
groups_auth: boolean(),
|
||||||
authorization_groups: Ecto.Schema.embeds_many(Teams.AuthorizationGroup.t()),
|
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()),
|
secrets: Ecto.Schema.has_many(Secrets.Secret.t()),
|
||||||
agent_keys: Ecto.Schema.has_many(Teams.AgentKey.t()),
|
agent_keys: Ecto.Schema.has_many(Teams.AgentKey.t()),
|
||||||
environment_variables: Ecto.Schema.has_many(Teams.EnvironmentVariable.t())
|
environment_variables: Ecto.Schema.has_many(Teams.EnvironmentVariable.t())
|
||||||
|
@ -29,11 +30,13 @@ defmodule Livebook.Teams.DeploymentGroup do
|
||||||
field :url, :string
|
field :url, :string
|
||||||
field :teams_auth, :boolean, default: true
|
field :teams_auth, :boolean, default: true
|
||||||
field :groups_auth, :boolean, default: false
|
field :groups_auth, :boolean, default: false
|
||||||
|
field :deploy_auth, :boolean, default: false
|
||||||
|
|
||||||
has_many :secrets, Secrets.Secret
|
has_many :secrets, Secrets.Secret
|
||||||
has_many :agent_keys, Teams.AgentKey
|
has_many :agent_keys, Teams.AgentKey
|
||||||
has_many :environment_variables, Teams.EnvironmentVariable
|
has_many :environment_variables, Teams.EnvironmentVariable
|
||||||
embeds_many :authorization_groups, Teams.AuthorizationGroup
|
embeds_many :authorization_groups, Teams.AuthorizationGroup
|
||||||
|
embeds_many :deployment_users, Teams.DeploymentUser
|
||||||
end
|
end
|
||||||
|
|
||||||
def changeset(deployment_group, attrs \\ %{}) do
|
def changeset(deployment_group, attrs \\ %{}) do
|
||||||
|
|
14
lib/livebook/teams/deployment_user.ex
Normal file
14
lib/livebook/teams/deployment_user.ex
Normal file
|
@ -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
|
|
@ -8,6 +8,7 @@ defmodule Livebook.Teams.Requests do
|
||||||
@deploy_key_prefix Teams.Constants.deploy_key_prefix()
|
@deploy_key_prefix Teams.Constants.deploy_key_prefix()
|
||||||
@error_message "Something went wrong, try again later or please file a bug if it persists"
|
@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_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 api_result :: {:ok, map()} | error_result()
|
||||||
@typep error_result :: {:error, map() | String.t()} | {:transport_error, String.t()}
|
@typep error_result :: {:error, map() | String.t()} | {:transport_error, String.t()}
|
||||||
|
@ -300,6 +301,11 @@ defmodule Livebook.Teams.Requests do
|
||||||
defp upload(path, content, params, team) do
|
defp upload(path, content, params, team) do
|
||||||
build_req(team)
|
build_req(team)
|
||||||
|> Req.Request.put_header("content-length", "#{byte_size(content)}")
|
|> 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)
|
|> Req.post(url: path, params: params, body: content)
|
||||||
|> handle_response()
|
|> handle_response()
|
||||||
|> dispatch_messages(team)
|
|> dispatch_messages(team)
|
||||||
|
@ -337,10 +343,20 @@ defmodule Livebook.Teams.Requests do
|
||||||
|
|
||||||
defp handle_response(response) do
|
defp handle_response(response) do
|
||||||
case response do
|
case response do
|
||||||
{:ok, %{status: status} = response} when status in 200..299 -> {:ok, response.body}
|
{:ok, %{status: status} = response} when status in 200..299 ->
|
||||||
{:ok, %{status: status} = response} when status in [410, 422] -> return_error(response)
|
{:ok, response.body}
|
||||||
{:ok, %{status: 401}} -> {:transport_error, @unauthorized_error_message}
|
|
||||||
_otherwise -> {:transport_error, @error_message}
|
{: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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -81,6 +81,7 @@ defmodule LivebookWeb.SessionLive.AppTeamsLive do
|
||||||
messages={@messages}
|
messages={@messages}
|
||||||
action={@action}
|
action={@action}
|
||||||
initial?={@initial?}
|
initial?={@initial?}
|
||||||
|
authorized={@authorized}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
@ -162,7 +163,12 @@ defmodule LivebookWeb.SessionLive.AppTeamsLive do
|
||||||
<.remix_icon icon="rocket-line" /> Deploy
|
<.remix_icon icon="rocket-line" /> Deploy
|
||||||
</.button>
|
</.button>
|
||||||
<% else %>
|
<% else %>
|
||||||
<.button color="blue" outlined phx-click="deploy_app">
|
<.button
|
||||||
|
disabled={!@authorized[@deployment_group.id]}
|
||||||
|
color="blue"
|
||||||
|
outlined
|
||||||
|
phx-click="deploy_app"
|
||||||
|
>
|
||||||
<.remix_icon icon="rocket-line" /> Deploy anyway
|
<.remix_icon icon="rocket-line" /> Deploy anyway
|
||||||
</.button>
|
</.button>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
@ -198,6 +204,7 @@ defmodule LivebookWeb.SessionLive.AppTeamsLive do
|
||||||
deployment_group={@deployment_group}
|
deployment_group={@deployment_group}
|
||||||
num_agents={@num_agents}
|
num_agents={@num_agents}
|
||||||
num_app_deployments={@num_app_deployments}
|
num_app_deployments={@num_app_deployments}
|
||||||
|
authorized={@authorized[@deployment_group.id]}
|
||||||
active
|
active
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -224,7 +231,12 @@ defmodule LivebookWeb.SessionLive.AppTeamsLive do
|
||||||
<.button color="blue" phx-click="go_add_agent">
|
<.button color="blue" phx-click="go_add_agent">
|
||||||
<.remix_icon icon="add-line" /> Add app server
|
<.remix_icon icon="add-line" /> Add app server
|
||||||
</.button>
|
</.button>
|
||||||
<.button color="blue" outlined phx-click="deploy_app">
|
<.button
|
||||||
|
disabled={!@authorized[@deployment_group.id]}
|
||||||
|
color="blue"
|
||||||
|
outlined
|
||||||
|
phx-click="deploy_app"
|
||||||
|
>
|
||||||
<.remix_icon icon="rocket-line" /> Deploy anyway
|
<.remix_icon icon="rocket-line" /> Deploy anyway
|
||||||
</.button>
|
</.button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -250,6 +262,7 @@ defmodule LivebookWeb.SessionLive.AppTeamsLive do
|
||||||
deployment_group={deployment_group}
|
deployment_group={deployment_group}
|
||||||
num_agents={@num_agents}
|
num_agents={@num_agents}
|
||||||
num_app_deployments={@num_app_deployments}
|
num_app_deployments={@num_app_deployments}
|
||||||
|
authorized={@authorized[deployment_group.id]}
|
||||||
selectable
|
selectable
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -301,6 +314,7 @@ defmodule LivebookWeb.SessionLive.AppTeamsLive do
|
||||||
|
|
||||||
attr :active, :boolean, default: false
|
attr :active, :boolean, default: false
|
||||||
attr :selectable, :boolean, default: false
|
attr :selectable, :boolean, default: false
|
||||||
|
attr :authorized, :boolean, default: true
|
||||||
attr :deployment_group, :map, required: true
|
attr :deployment_group, :map, required: true
|
||||||
attr :num_agents, :map, required: true
|
attr :num_agents, :map, required: true
|
||||||
attr :num_app_deployments, :map, required: true
|
attr :num_app_deployments, :map, required: true
|
||||||
|
@ -310,29 +324,45 @@ defmodule LivebookWeb.SessionLive.AppTeamsLive do
|
||||||
~H"""
|
~H"""
|
||||||
<div
|
<div
|
||||||
class={[
|
class={[
|
||||||
"border p-3 rounded-lg",
|
"border p-3 rounded-lg relative",
|
||||||
@selectable && "cursor-pointer",
|
cond do
|
||||||
if(@active,
|
!@authorized -> "!block cursor-not-allowed tooltip top opacity-50 bg-gray-50"
|
||||||
do: "border-blue-600 bg-blue-50",
|
@selectable -> "cursor-pointer border-blue-600 bg-blue-50"
|
||||||
else: "border-gray-200"
|
true -> "cursor-pointer border-gray-200"
|
||||||
)
|
end
|
||||||
]}
|
]}
|
||||||
phx-click={@selectable && "select_deployment_group"}
|
data-tooltip={!@authorized && "You are not authorized to deploy to this deployment group"}
|
||||||
|
phx-click={@selectable && @authorized && "select_deployment_group"}
|
||||||
phx-value-id={@deployment_group.id}
|
phx-value-id={@deployment_group.id}
|
||||||
{@rest}
|
{@rest}
|
||||||
>
|
>
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<div class="flex gap-2 items-center text-gray-700">
|
<div class="flex gap-2 items-center">
|
||||||
<h3 class="text-sm">
|
<h3 class={[
|
||||||
|
"text-sm",
|
||||||
|
if(@authorized, do: "text-gray-700", else: "text-gray-500")
|
||||||
|
]}>
|
||||||
<span class="font-semibold">{@deployment_group.name}</span>
|
<span class="font-semibold">{@deployment_group.name}</span>
|
||||||
<span :if={url = @deployment_group.url}>({url})</span>
|
<span :if={url = @deployment_group.url}>({url})</span>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2 flex-shrink-0">
|
||||||
<div class="text-sm text-gray-700 border-l border-gray-300 pl-2">
|
<div class={[
|
||||||
|
"text-sm border-l pl-2",
|
||||||
|
if(@authorized,
|
||||||
|
do: "border-gray-300 text-gray-700",
|
||||||
|
else: "border-gray-300 text-gray-500"
|
||||||
|
)
|
||||||
|
]}>
|
||||||
App servers: {@num_agents[@deployment_group.id] || 0}
|
App servers: {@num_agents[@deployment_group.id] || 0}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-700 border-l border-gray-300 pl-2">
|
<div class={[
|
||||||
|
"text-sm border-l pl-2",
|
||||||
|
if(@authorized,
|
||||||
|
do: "border-gray-300 text-gray-700",
|
||||||
|
else: "border-gray-300 text-gray-500"
|
||||||
|
)
|
||||||
|
]}>
|
||||||
Apps deployed: {@num_app_deployments[@deployment_group.id] || 0}
|
Apps deployed: {@num_app_deployments[@deployment_group.id] || 0}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -440,6 +470,11 @@ defmodule LivebookWeb.SessionLive.AppTeamsLive do
|
||||||
{:noreply, socket |> assign_app_deployments() |> assign_app_deployment()}
|
{:noreply, socket |> assign_app_deployments() |> assign_app_deployment()}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_info({:deployment_users_updated, deployment_group}, socket)
|
||||||
|
when deployment_group.hub_id == socket.assigns.hub.id do
|
||||||
|
{:noreply, assign_deployment_groups(socket)}
|
||||||
|
end
|
||||||
|
|
||||||
def handle_info(
|
def handle_info(
|
||||||
{:operation, {:set_notebook_deployment_group, _client_id, deployment_group_id}},
|
{:operation, {:set_notebook_deployment_group, _client_id, deployment_group_id}},
|
||||||
socket
|
socket
|
||||||
|
@ -453,13 +488,20 @@ defmodule LivebookWeb.SessionLive.AppTeamsLive do
|
||||||
def handle_info(_message, socket), do: {:noreply, socket}
|
def handle_info(_message, socket), do: {:noreply, socket}
|
||||||
|
|
||||||
defp assign_deployment_groups(socket) do
|
defp assign_deployment_groups(socket) do
|
||||||
|
hub = socket.assigns.hub
|
||||||
|
|
||||||
deployment_groups =
|
deployment_groups =
|
||||||
socket.assigns.hub
|
hub
|
||||||
|> Teams.get_deployment_groups()
|
|> Teams.get_deployment_groups()
|
||||||
|> Enum.filter(&(&1.mode == :online))
|
|> Enum.filter(&(&1.mode == :online))
|
||||||
|> Enum.sort_by(& &1.name)
|
|> Enum.sort_by(& &1.name)
|
||||||
|
|
||||||
assign(socket, deployment_groups: deployment_groups)
|
authorized =
|
||||||
|
for deployment_group <- deployment_groups, into: %{} do
|
||||||
|
{deployment_group.id, Teams.user_can_deploy?(hub, deployment_group)}
|
||||||
|
end
|
||||||
|
|
||||||
|
assign(socket, deployment_groups: deployment_groups, authorized: authorized)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp assign_app_deployments(socket) do
|
defp assign_app_deployments(socket) do
|
||||||
|
|
|
@ -24,4 +24,10 @@ defmodule LivebookProto.DeploymentGroup do
|
||||||
json_name: "authorizationGroups"
|
json_name: "authorizationGroups"
|
||||||
|
|
||||||
field :groups_auth, 13, type: :bool, json_name: "groupsAuth"
|
field :groups_auth, 13, type: :bool, json_name: "groupsAuth"
|
||||||
|
field :deploy_auth, 14, type: :bool, json_name: "deployAuth"
|
||||||
|
|
||||||
|
field :deployment_users, 15,
|
||||||
|
repeated: true,
|
||||||
|
type: LivebookProto.DeploymentUser,
|
||||||
|
json_name: "deploymentUsers"
|
||||||
end
|
end
|
||||||
|
|
|
@ -23,4 +23,10 @@ defmodule LivebookProto.DeploymentGroupUpdated do
|
||||||
json_name: "authorizationGroups"
|
json_name: "authorizationGroups"
|
||||||
|
|
||||||
field :groups_auth, 12, type: :bool, json_name: "groupsAuth"
|
field :groups_auth, 12, type: :bool, json_name: "groupsAuth"
|
||||||
|
field :deploy_auth, 13, type: :bool, json_name: "deployAuth"
|
||||||
|
|
||||||
|
field :deployment_users, 14,
|
||||||
|
repeated: true,
|
||||||
|
type: LivebookProto.DeploymentUser,
|
||||||
|
json_name: "deploymentUsers"
|
||||||
end
|
end
|
||||||
|
|
6
proto/lib/livebook_proto/deployment_user.pb.ex
Normal file
6
proto/lib/livebook_proto/deployment_user.pb.ex
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
defmodule LivebookProto.DeploymentUser do
|
||||||
|
use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3
|
||||||
|
|
||||||
|
field :user_id, 1, type: :string, json_name: "userId"
|
||||||
|
field :deployment_group_id, 2, type: :string, json_name: "deploymentGroupId"
|
||||||
|
end
|
|
@ -68,6 +68,8 @@ message DeploymentGroup {
|
||||||
bool teams_auth = 11;
|
bool teams_auth = 11;
|
||||||
repeated AuthorizationGroup authorization_groups = 12;
|
repeated AuthorizationGroup authorization_groups = 12;
|
||||||
bool groups_auth = 13;
|
bool groups_auth = 13;
|
||||||
|
bool deploy_auth = 14;
|
||||||
|
repeated DeploymentUser deployment_users = 15;
|
||||||
}
|
}
|
||||||
|
|
||||||
message DeploymentGroupCreated {
|
message DeploymentGroupCreated {
|
||||||
|
@ -95,6 +97,8 @@ message DeploymentGroupUpdated {
|
||||||
bool teams_auth = 10;
|
bool teams_auth = 10;
|
||||||
repeated AuthorizationGroup authorization_groups = 11;
|
repeated AuthorizationGroup authorization_groups = 11;
|
||||||
bool groups_auth = 12;
|
bool groups_auth = 12;
|
||||||
|
bool deploy_auth = 13;
|
||||||
|
repeated DeploymentUser deployment_users = 14;
|
||||||
}
|
}
|
||||||
|
|
||||||
message DeploymentGroupDeleted {
|
message DeploymentGroupDeleted {
|
||||||
|
@ -213,6 +217,11 @@ message AuthorizationGroup {
|
||||||
string group_name = 2;
|
string group_name = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message DeploymentUser {
|
||||||
|
string user_id = 1;
|
||||||
|
string deployment_group_id = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message Event {
|
message Event {
|
||||||
oneof type {
|
oneof type {
|
||||||
SecretCreated secret_created = 1;
|
SecretCreated secret_created = 1;
|
||||||
|
@ -258,5 +267,4 @@ message BillingStatusCanceling {
|
||||||
int64 cancel_at = 1;
|
int64 cancel_at = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message BillingStatusCanceled {
|
message BillingStatusCanceled {}
|
||||||
}
|
|
||||||
|
|
|
@ -112,6 +112,58 @@ defmodule LivebookCLI.Integration.DeployTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "fails with unauthorized deploy key",
|
||||||
|
%{team: team, node: node, org: org, tmp_dir: tmp_dir} do
|
||||||
|
title = "Test CLI Deploy App"
|
||||||
|
slug = Utils.random_short_id()
|
||||||
|
app_path = Path.join(tmp_dir, "#{slug}.livemd")
|
||||||
|
{key, _} = TeamsRPC.create_deploy_key(node, org: org)
|
||||||
|
|
||||||
|
deployment_group =
|
||||||
|
TeamsRPC.create_deployment_group(node, org: org, url: @url, deploy_auth: true)
|
||||||
|
|
||||||
|
hub_id = team.id
|
||||||
|
deployment_group_id = to_string(deployment_group.id)
|
||||||
|
|
||||||
|
stamp_notebook(app_path, """
|
||||||
|
<!-- livebook:{"app_settings":{"access_type":"public","slug":"#{slug}"},"hub_id":"#{hub_id}"} -->
|
||||||
|
|
||||||
|
# #{title}
|
||||||
|
|
||||||
|
## Test Section
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
IO.puts("Hello from CLI deployed app!")
|
||||||
|
```
|
||||||
|
""")
|
||||||
|
|
||||||
|
output =
|
||||||
|
ExUnit.CaptureIO.capture_io(fn ->
|
||||||
|
assert_raise(LivebookCLI.Error, "Some app deployments failed.", fn ->
|
||||||
|
assert deploy(
|
||||||
|
key,
|
||||||
|
team.teams_key,
|
||||||
|
deployment_group.id,
|
||||||
|
app_path
|
||||||
|
) == :ok
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert output =~ "* Preparing to deploy notebook #{slug}.livemd"
|
||||||
|
|
||||||
|
assert output =~
|
||||||
|
"* Test CLI Deploy App failed to deploy. Transport error: You are not authorized to perform this action, make sure you have the access to deploy apps to this deployment group"
|
||||||
|
|
||||||
|
refute_receive {:app_deployment_started,
|
||||||
|
%{
|
||||||
|
title: ^title,
|
||||||
|
slug: ^slug,
|
||||||
|
deployment_group_id: ^deployment_group_id,
|
||||||
|
hub_id: ^hub_id,
|
||||||
|
deployed_by: "CLI"
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
|
||||||
test "fails with invalid deploy key", %{team: team, node: node, org: org, tmp_dir: tmp_dir} do
|
test "fails with invalid deploy key", %{team: team, node: node, org: org, tmp_dir: tmp_dir} do
|
||||||
slug = Utils.random_short_id()
|
slug = Utils.random_short_id()
|
||||||
app_path = Path.join(tmp_dir, "#{slug}.livemd")
|
app_path = Path.join(tmp_dir, "#{slug}.livemd")
|
||||||
|
|
|
@ -571,6 +571,56 @@ defmodule LivebookWeb.Integration.SessionLiveTest do
|
||||||
"App deployment created successfully"
|
"App deployment created successfully"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "shows tooltip message if user is unauthorized to deploy apps",
|
||||||
|
%{team: team, node: node, org: org, conn: conn, session: session} do
|
||||||
|
Session.set_notebook_hub(session.pid, team.id)
|
||||||
|
|
||||||
|
deployment_group = TeamsRPC.create_deployment_group(node, mode: :online, org: org)
|
||||||
|
id = to_string(deployment_group.id)
|
||||||
|
assert_receive {:deployment_group_created, %{id: ^id, deploy_auth: false}}
|
||||||
|
|
||||||
|
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
|
||||||
|
|
||||||
|
view
|
||||||
|
|> element("a", "Deploy with Livebook Teams")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# Step: configuring valid app settings
|
||||||
|
|
||||||
|
assert render(view) =~ "You must configure your app before deploying it."
|
||||||
|
|
||||||
|
slug = Livebook.Utils.random_short_id()
|
||||||
|
|
||||||
|
view
|
||||||
|
|> element(~s/#app-settings-modal form/)
|
||||||
|
|> render_submit(%{"app_settings" => %{"slug" => slug}})
|
||||||
|
|
||||||
|
# From this point forward we are in a child LV
|
||||||
|
view = find_live_child(view, "app-teams")
|
||||||
|
assert render(view) =~ "App deployment with Livebook Teams"
|
||||||
|
|
||||||
|
# show the deployment group being able to select to deploy an app
|
||||||
|
assert has_element?(
|
||||||
|
view,
|
||||||
|
~s/[phx-click="select_deployment_group"][phx-value-id="#{deployment_group.id}"]/
|
||||||
|
)
|
||||||
|
|
||||||
|
# then, we update the deployment group, so it will
|
||||||
|
# update the view and show the tooltip with unauthorized error message
|
||||||
|
{:ok, deployment_group} = TeamsRPC.toggle_deployment_authorization(node, deployment_group)
|
||||||
|
assert_receive {:deployment_group_updated, %{id: ^id, deploy_auth: true}}
|
||||||
|
|
||||||
|
refute has_element?(
|
||||||
|
view,
|
||||||
|
~s/[phx-click="select_deployment_group"][phx-value-id="#{deployment_group.id}"]/
|
||||||
|
)
|
||||||
|
|
||||||
|
assert has_element?(
|
||||||
|
view,
|
||||||
|
~s/[data-tooltip="You are not authorized to deploy to this deployment group"][phx-value-id="#{deployment_group.id}"]/
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
test "shows an error when the deployment size is higher than the maximum size of 20MB",
|
test "shows an error when the deployment size is higher than the maximum size of 20MB",
|
||||||
%{team: team, conn: conn, session: session} do
|
%{team: team, conn: conn, session: session} do
|
||||||
Session.set_notebook_hub(session.pid, team.id)
|
Session.set_notebook_hub(session.pid, team.id)
|
||||||
|
@ -603,5 +653,42 @@ defmodule LivebookWeb.Integration.SessionLiveTest do
|
||||||
assert render(view) =~
|
assert render(view) =~
|
||||||
"Failed to pack files: the notebook and its attachments have exceeded the maximum size of 20MB"
|
"Failed to pack files: the notebook and its attachments have exceeded the maximum size of 20MB"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "shows an error when the deployment is unauthorized",
|
||||||
|
%{team: team, org: org, node: node, conn: conn, session: session} do
|
||||||
|
Session.set_notebook_hub(session.pid, team.id)
|
||||||
|
|
||||||
|
slug = Livebook.Utils.random_short_id()
|
||||||
|
app_settings = %{Livebook.Notebook.AppSettings.new() | slug: slug}
|
||||||
|
Session.set_app_settings(session.pid, app_settings)
|
||||||
|
|
||||||
|
deployment_group = TeamsRPC.create_deployment_group(node, mode: :online, org: org)
|
||||||
|
id = to_string(deployment_group.id)
|
||||||
|
assert_receive {:deployment_group_created, %{id: ^id, deploy_auth: false}}
|
||||||
|
|
||||||
|
{:ok, _} = TeamsRPC.toggle_deployment_authorization(node, deployment_group)
|
||||||
|
assert_receive {:deployment_group_updated, %{id: ^id, deploy_auth: true}}
|
||||||
|
|
||||||
|
Session.set_notebook_deployment_group(session.pid, id)
|
||||||
|
assert_receive {:operation, {:set_notebook_deployment_group, _, ^id}}
|
||||||
|
|
||||||
|
%{files_dir: files_dir} = session
|
||||||
|
image_file = FileSystem.File.resolve(files_dir, "image.jpg")
|
||||||
|
:ok = FileSystem.File.write(image_file, :crypto.strong_rand_bytes(1024 * 1024))
|
||||||
|
Session.add_file_entries(session.pid, [%{type: :attachment, name: "image.jpg"}])
|
||||||
|
|
||||||
|
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}/app-teams")
|
||||||
|
|
||||||
|
# From this point forward we are in a child LV
|
||||||
|
view = find_live_child(view, "app-teams")
|
||||||
|
assert render(view) =~ "App deployment with Livebook Teams"
|
||||||
|
|
||||||
|
view
|
||||||
|
|> element("button", "Deploy")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
assert render(view) =~
|
||||||
|
"You are not authorized to perform this action, make sure you have the access to deploy apps to this deployment group"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -208,4 +208,8 @@ defmodule Livebook.TeamsRPC do
|
||||||
def toggle_groups_authorization(node, deployment_group) do
|
def toggle_groups_authorization(node, deployment_group) do
|
||||||
:erpc.call(node, TeamsRPC, :toggle_groups_authorization, [deployment_group])
|
:erpc.call(node, TeamsRPC, :toggle_groups_authorization, [deployment_group])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def toggle_deployment_authorization(node, deployment_group) do
|
||||||
|
:erpc.call(node, TeamsRPC, :toggle_deployment_authorization, [deployment_group])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Add table
Reference in a new issue