mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-11-11 22:51:43 +08:00
Add shared mode to Livebook sessions (#1128)
* add shared mode (working)
* add markdown write permission
* Add authorize handle_event
* Revert "add shared mode (working)"
This reverts commit c1e9ebac4d.
* Add assert_policy!
* Apply suggestions code review
* Apply suggestions from code review
* Apply suggestions from code reviews (redirect)
* Apply suggestions from code review
* Apply suggestions from code review
* Apply suggestions from code review
* Apply suggestions from code review
* Add PolicyHook
* remove assert_live_action_access!/1
* Apply suggestions from code review
* Apply suggestions from code review
* Update lib/livebook_web/live/session_live.ex
Co-authored-by: Jonatan Kłosko <jonatanklosko@gmail.com>
This commit is contained in:
parent
19016dcc54
commit
d11084d32f
3 changed files with 68 additions and 24 deletions
7
lib/livebook_web/live/hooks/policy_hook.ex
Normal file
7
lib/livebook_web/live/hooks/policy_hook.ex
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
defmodule LivebookWeb.PolicyHook do
|
||||||
|
import Phoenix.LiveView
|
||||||
|
|
||||||
|
def on_mount(:private, _params, _session, socket) do
|
||||||
|
{:cont, socket |> assign(:policy, %{read: true, execute: true, comment: true, edit: true})}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -549,12 +549,14 @@ defmodule LivebookWeb.SessionLive do
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_params(%{"cell_id" => cell_id}, _url, socket) do
|
def handle_params(%{"cell_id" => cell_id}, _url, socket)
|
||||||
|
when socket.assigns.live_action in [:cell_settings, :cell_upload] do
|
||||||
{:ok, cell, _} = Notebook.fetch_cell_and_section(socket.private.data.notebook, cell_id)
|
{:ok, cell, _} = Notebook.fetch_cell_and_section(socket.private.data.notebook, cell_id)
|
||||||
{:noreply, assign(socket, cell: cell)}
|
{:noreply, assign(socket, cell: cell)}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_params(%{"section_id" => section_id}, _url, socket) do
|
def handle_params(%{"section_id" => section_id}, _url, socket)
|
||||||
|
when socket.assigns.live_action == :delete_section do
|
||||||
{:ok, section} = Notebook.fetch_section(socket.private.data.notebook, section_id)
|
{:ok, section} = Notebook.fetch_section(socket.private.data.notebook, section_id)
|
||||||
first_section_id = hd(socket.private.data.notebook.sections).id
|
first_section_id = hd(socket.private.data.notebook.sections).id
|
||||||
{:noreply, assign(socket, section: section, first_section_id: first_section_id)}
|
{:noreply, assign(socket, section: section, first_section_id: first_section_id)}
|
||||||
|
|
@ -565,17 +567,22 @@ defmodule LivebookWeb.SessionLive do
|
||||||
_url,
|
_url,
|
||||||
%{assigns: %{live_action: :catch_all}} = socket
|
%{assigns: %{live_action: :catch_all}} = socket
|
||||||
) do
|
) do
|
||||||
path_parts =
|
if socket.assigns.policy.edit do
|
||||||
Enum.map(path_parts, fn
|
path_parts =
|
||||||
"__parent__" -> ".."
|
Enum.map(path_parts, fn
|
||||||
part -> part
|
"__parent__" -> ".."
|
||||||
end)
|
part -> part
|
||||||
|
end)
|
||||||
|
|
||||||
path = Path.join(path_parts)
|
path = Path.join(path_parts)
|
||||||
{:noreply, handle_relative_path(socket, path)}
|
{:noreply, handle_relative_path(socket, path)}
|
||||||
|
else
|
||||||
|
{:noreply, socket |> put_flash(:error, "No access to navigate") |> redirect_to_self()}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_params(%{"tab" => tab}, _url, socket) do
|
def handle_params(%{"tab" => tab}, _url, socket)
|
||||||
|
when socket.assigns.live_action == :export do
|
||||||
{:noreply, assign(socket, tab: tab)}
|
{:noreply, assign(socket, tab: tab)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -585,6 +592,7 @@ defmodule LivebookWeb.SessionLive do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("append_section", %{}, socket) do
|
def handle_event("append_section", %{}, socket) do
|
||||||
|
assert_policy!(socket, :edit)
|
||||||
idx = length(socket.private.data.notebook.sections)
|
idx = length(socket.private.data.notebook.sections)
|
||||||
Session.insert_section(socket.assigns.session.pid, idx)
|
Session.insert_section(socket.assigns.session.pid, idx)
|
||||||
|
|
||||||
|
|
@ -592,6 +600,8 @@ defmodule LivebookWeb.SessionLive do
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("insert_section_below", params, socket) do
|
def handle_event("insert_section_below", params, socket) do
|
||||||
|
assert_policy!(socket, :edit)
|
||||||
|
|
||||||
with {:ok, section, index} <-
|
with {:ok, section, index} <-
|
||||||
section_with_next_index(
|
section_with_next_index(
|
||||||
socket.private.data.notebook,
|
socket.private.data.notebook,
|
||||||
|
|
@ -609,18 +619,21 @@ defmodule LivebookWeb.SessionLive do
|
||||||
%{"section_id" => section_id, "parent_id" => parent_id},
|
%{"section_id" => section_id, "parent_id" => parent_id},
|
||||||
socket
|
socket
|
||||||
) do
|
) do
|
||||||
|
assert_policy!(socket, :edit)
|
||||||
Session.set_section_parent(socket.assigns.session.pid, section_id, parent_id)
|
Session.set_section_parent(socket.assigns.session.pid, section_id, parent_id)
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("unset_section_parent", %{"section_id" => section_id}, socket) do
|
def handle_event("unset_section_parent", %{"section_id" => section_id}, socket) do
|
||||||
|
assert_policy!(socket, :edit)
|
||||||
Session.unset_section_parent(socket.assigns.session.pid, section_id)
|
Session.unset_section_parent(socket.assigns.session.pid, section_id)
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("insert_cell_below", params, socket) do
|
def handle_event("insert_cell_below", params, socket) do
|
||||||
|
assert_policy!(socket, :edit)
|
||||||
{type, attrs} = cell_type_and_attrs_from_params(params)
|
{type, attrs} = cell_type_and_attrs_from_params(params)
|
||||||
|
|
||||||
with {:ok, section, index} <-
|
with {:ok, section, index} <-
|
||||||
|
|
@ -636,12 +649,14 @@ defmodule LivebookWeb.SessionLive do
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("delete_cell", %{"cell_id" => cell_id}, socket) do
|
def handle_event("delete_cell", %{"cell_id" => cell_id}, socket) do
|
||||||
|
assert_policy!(socket, :edit)
|
||||||
Session.delete_cell(socket.assigns.session.pid, cell_id)
|
Session.delete_cell(socket.assigns.session.pid, cell_id)
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("set_notebook_name", %{"value" => name}, socket) do
|
def handle_event("set_notebook_name", %{"value" => name}, socket) do
|
||||||
|
assert_policy!(socket, :edit)
|
||||||
name = normalize_name(name)
|
name = normalize_name(name)
|
||||||
Session.set_notebook_name(socket.assigns.session.pid, name)
|
Session.set_notebook_name(socket.assigns.session.pid, name)
|
||||||
|
|
||||||
|
|
@ -649,6 +664,7 @@ defmodule LivebookWeb.SessionLive do
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("set_section_name", %{"metadata" => section_id, "value" => name}, socket) do
|
def handle_event("set_section_name", %{"metadata" => section_id, "value" => name}, socket) do
|
||||||
|
assert_policy!(socket, :edit)
|
||||||
name = normalize_name(name)
|
name = normalize_name(name)
|
||||||
Session.set_section_name(socket.assigns.session.pid, section_id, name)
|
Session.set_section_name(socket.assigns.session.pid, section_id, name)
|
||||||
|
|
||||||
|
|
@ -660,6 +676,7 @@ defmodule LivebookWeb.SessionLive do
|
||||||
%{"cell_id" => cell_id, "tag" => tag, "delta" => delta, "revision" => revision},
|
%{"cell_id" => cell_id, "tag" => tag, "delta" => delta, "revision" => revision},
|
||||||
socket
|
socket
|
||||||
) do
|
) do
|
||||||
|
assert_policy!(socket, :edit)
|
||||||
tag = String.to_atom(tag)
|
tag = String.to_atom(tag)
|
||||||
delta = Delta.from_compressed(delta)
|
delta = Delta.from_compressed(delta)
|
||||||
Session.apply_cell_delta(socket.assigns.session.pid, cell_id, tag, delta, revision)
|
Session.apply_cell_delta(socket.assigns.session.pid, cell_id, tag, delta, revision)
|
||||||
|
|
@ -672,6 +689,7 @@ defmodule LivebookWeb.SessionLive do
|
||||||
%{"cell_id" => cell_id, "tag" => tag, "revision" => revision},
|
%{"cell_id" => cell_id, "tag" => tag, "revision" => revision},
|
||||||
socket
|
socket
|
||||||
) do
|
) do
|
||||||
|
assert_policy!(socket, :read)
|
||||||
tag = String.to_atom(tag)
|
tag = String.to_atom(tag)
|
||||||
Session.report_cell_revision(socket.assigns.session.pid, cell_id, tag, revision)
|
Session.report_cell_revision(socket.assigns.session.pid, cell_id, tag, revision)
|
||||||
|
|
||||||
|
|
@ -679,6 +697,7 @@ defmodule LivebookWeb.SessionLive do
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("move_cell", %{"cell_id" => cell_id, "offset" => offset}, socket) do
|
def handle_event("move_cell", %{"cell_id" => cell_id, "offset" => offset}, socket) do
|
||||||
|
assert_policy!(socket, :edit)
|
||||||
offset = ensure_integer(offset)
|
offset = ensure_integer(offset)
|
||||||
Session.move_cell(socket.assigns.session.pid, cell_id, offset)
|
Session.move_cell(socket.assigns.session.pid, cell_id, offset)
|
||||||
|
|
||||||
|
|
@ -686,6 +705,7 @@ defmodule LivebookWeb.SessionLive do
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("move_section", %{"section_id" => section_id, "offset" => offset}, socket) do
|
def handle_event("move_section", %{"section_id" => section_id, "offset" => offset}, socket) do
|
||||||
|
assert_policy!(socket, :edit)
|
||||||
offset = ensure_integer(offset)
|
offset = ensure_integer(offset)
|
||||||
Session.move_section(socket.assigns.session.pid, section_id, offset)
|
Session.move_section(socket.assigns.session.pid, section_id, offset)
|
||||||
|
|
||||||
|
|
@ -693,6 +713,8 @@ defmodule LivebookWeb.SessionLive do
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("delete_section", %{"section_id" => section_id}, socket) do
|
def handle_event("delete_section", %{"section_id" => section_id}, socket) do
|
||||||
|
assert_policy!(socket, :edit)
|
||||||
|
|
||||||
socket =
|
socket =
|
||||||
case Notebook.fetch_section(socket.private.data.notebook, section_id) do
|
case Notebook.fetch_section(socket.private.data.notebook, section_id) do
|
||||||
{:ok, %{cells: []} = section} ->
|
{:ok, %{cells: []} = section} ->
|
||||||
|
|
@ -718,6 +740,7 @@ defmodule LivebookWeb.SessionLive do
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("convert_smart_cell", %{"cell_id" => cell_id}, socket) do
|
def handle_event("convert_smart_cell", %{"cell_id" => cell_id}, socket) do
|
||||||
|
assert_policy!(socket, :edit)
|
||||||
Session.convert_smart_cell(socket.assigns.session.pid, cell_id)
|
Session.convert_smart_cell(socket.assigns.session.pid, cell_id)
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
|
|
@ -728,6 +751,8 @@ defmodule LivebookWeb.SessionLive do
|
||||||
%{"kind" => kind, "variant_idx" => variant_idx},
|
%{"kind" => kind, "variant_idx" => variant_idx},
|
||||||
socket
|
socket
|
||||||
) do
|
) do
|
||||||
|
assert_policy!(socket, :edit)
|
||||||
|
|
||||||
with %{requirement: %{variants: variants}} <-
|
with %{requirement: %{variants: variants}} <-
|
||||||
Enum.find(socket.private.data.smart_cell_definitions, &(&1.kind == kind)),
|
Enum.find(socket.private.data.smart_cell_definitions, &(&1.kind == kind)),
|
||||||
{:ok, %{dependencies: dependencies}} <- Enum.fetch(variants, variant_idx) do
|
{:ok, %{dependencies: dependencies}} <- Enum.fetch(variants, variant_idx) do
|
||||||
|
|
@ -744,6 +769,7 @@ defmodule LivebookWeb.SessionLive do
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("queue_cell_evaluation", %{"cell_id" => cell_id}, socket) do
|
def handle_event("queue_cell_evaluation", %{"cell_id" => cell_id}, socket) do
|
||||||
|
assert_policy!(socket, :execute)
|
||||||
data = socket.private.data
|
data = socket.private.data
|
||||||
|
|
||||||
{status, socket} =
|
{status, socket} =
|
||||||
|
|
@ -763,24 +789,29 @@ defmodule LivebookWeb.SessionLive do
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("queue_section_evaluation", %{"section_id" => section_id}, socket) do
|
def handle_event("queue_section_evaluation", %{"section_id" => section_id}, socket) do
|
||||||
|
assert_policy!(socket, :execute)
|
||||||
Session.queue_section_evaluation(socket.assigns.session.pid, section_id)
|
Session.queue_section_evaluation(socket.assigns.session.pid, section_id)
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("queue_full_evaluation", %{"forced_cell_ids" => forced_cell_ids}, socket) do
|
def handle_event("queue_full_evaluation", %{"forced_cell_ids" => forced_cell_ids}, socket) do
|
||||||
|
assert_policy!(socket, :execute)
|
||||||
Session.queue_full_evaluation(socket.assigns.session.pid, forced_cell_ids)
|
Session.queue_full_evaluation(socket.assigns.session.pid, forced_cell_ids)
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("cancel_cell_evaluation", %{"cell_id" => cell_id}, socket) do
|
def handle_event("cancel_cell_evaluation", %{"cell_id" => cell_id}, socket) do
|
||||||
|
assert_policy!(socket, :execute)
|
||||||
Session.cancel_cell_evaluation(socket.assigns.session.pid, cell_id)
|
Session.cancel_cell_evaluation(socket.assigns.session.pid, cell_id)
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("save", %{}, socket) do
|
def handle_event("save", %{}, socket) do
|
||||||
|
assert_policy!(socket, :edit)
|
||||||
|
|
||||||
if socket.private.data.file do
|
if socket.private.data.file do
|
||||||
Session.save(socket.assigns.session.pid)
|
Session.save(socket.assigns.session.pid)
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
|
|
@ -793,16 +824,19 @@ defmodule LivebookWeb.SessionLive do
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("reconnect_runtime", %{}, socket) do
|
def handle_event("reconnect_runtime", %{}, socket) do
|
||||||
|
assert_policy!(socket, :execute)
|
||||||
{_, socket} = maybe_reconnect_runtime(socket)
|
{_, socket} = maybe_reconnect_runtime(socket)
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("connect_runtime", %{}, socket) do
|
def handle_event("connect_runtime", %{}, socket) do
|
||||||
|
assert_policy!(socket, :execute)
|
||||||
{_, socket} = connect_runtime(socket)
|
{_, socket} = connect_runtime(socket)
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("setup_default_runtime", %{}, socket) do
|
def handle_event("setup_default_runtime", %{}, socket) do
|
||||||
|
assert_policy!(socket, :execute)
|
||||||
{status, socket} = connect_runtime(socket)
|
{status, socket} = connect_runtime(socket)
|
||||||
|
|
||||||
if status == :ok do
|
if status == :ok do
|
||||||
|
|
@ -813,11 +847,14 @@ defmodule LivebookWeb.SessionLive do
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("disconnect_runtime", %{}, socket) do
|
def handle_event("disconnect_runtime", %{}, socket) do
|
||||||
|
assert_policy!(socket, :execute)
|
||||||
Session.disconnect_runtime(socket.assigns.session.pid)
|
Session.disconnect_runtime(socket.assigns.session.pid)
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("intellisense_request", %{"cell_id" => cell_id} = params, socket) do
|
def handle_event("intellisense_request", %{"cell_id" => cell_id} = params, socket) do
|
||||||
|
assert_policy!(socket, :read)
|
||||||
|
|
||||||
request =
|
request =
|
||||||
case params do
|
case params do
|
||||||
%{"type" => "completion", "hint" => hint} ->
|
%{"type" => "completion", "hint" => hint} ->
|
||||||
|
|
@ -864,6 +901,7 @@ defmodule LivebookWeb.SessionLive do
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("fork_session", %{}, socket) do
|
def handle_event("fork_session", %{}, socket) do
|
||||||
|
assert_policy!(socket, :edit)
|
||||||
%{pid: pid, images_dir: images_dir} = socket.assigns.session
|
%{pid: pid, images_dir: images_dir} = socket.assigns.session
|
||||||
# Fetch the data, as we don't keep cells' source in the state
|
# Fetch the data, as we don't keep cells' source in the state
|
||||||
data = Session.get_data(pid)
|
data = Session.get_data(pid)
|
||||||
|
|
@ -872,11 +910,14 @@ defmodule LivebookWeb.SessionLive do
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("erase_outputs", %{}, socket) do
|
def handle_event("erase_outputs", %{}, socket) do
|
||||||
|
assert_policy!(socket, :execute)
|
||||||
Session.erase_outputs(socket.assigns.session.pid)
|
Session.erase_outputs(socket.assigns.session.pid)
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("location_report", report, socket) do
|
def handle_event("location_report", report, socket) do
|
||||||
|
assert_policy!(socket, :read)
|
||||||
|
|
||||||
Phoenix.PubSub.broadcast_from(
|
Phoenix.PubSub.broadcast_from(
|
||||||
Livebook.PubSub,
|
Livebook.PubSub,
|
||||||
self(),
|
self(),
|
||||||
|
|
@ -887,19 +928,6 @@ defmodule LivebookWeb.SessionLive do
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("format_code", %{"code" => code}, socket) do
|
|
||||||
formatted =
|
|
||||||
try do
|
|
||||||
code
|
|
||||||
|> Code.format_string!()
|
|
||||||
|> IO.iodata_to_binary()
|
|
||||||
rescue
|
|
||||||
_ -> code
|
|
||||||
end
|
|
||||||
|
|
||||||
{:reply, %{code: formatted}, socket}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info({:operation, operation}, socket) do
|
def handle_info({:operation, operation}, socket) do
|
||||||
{:noreply, handle_operation(socket, operation)}
|
{:noreply, handle_operation(socket, operation)}
|
||||||
|
|
@ -1657,4 +1685,12 @@ defmodule LivebookWeb.SessionLive do
|
||||||
}}
|
}}
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp assert_policy!(socket, key) do
|
||||||
|
unless socket.assigns.policy |> Map.fetch!(key) do
|
||||||
|
raise "policy not allowed"
|
||||||
|
end
|
||||||
|
|
||||||
|
:ok
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,8 @@ defmodule LivebookWeb.Router do
|
||||||
get "/sessions/:id/assets/:hash/*file_parts", SessionController, :show_asset
|
get "/sessions/:id/assets/:hash/*file_parts", SessionController, :show_asset
|
||||||
end
|
end
|
||||||
|
|
||||||
live_session :default, on_mount: [LivebookWeb.AuthHook, LivebookWeb.UserHook] do
|
live_session :default,
|
||||||
|
on_mount: [LivebookWeb.AuthHook, LivebookWeb.UserHook, {LivebookWeb.PolicyHook, :private}] do
|
||||||
scope "/", LivebookWeb do
|
scope "/", LivebookWeb do
|
||||||
pipe_through [:browser, :auth]
|
pipe_through [:browser, :auth]
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue