Packages
livebook
0.19.6
0.19.8
0.19.7
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.6
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.3
0.17.2
0.17.1
0.17.0
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.14.0-rc.1
0.14.0-rc.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.2.0
0.1.2
0.1.1
0.1.0
Automate code & data workflows with interactive notebooks
Current section
Files
Jump to
Current section
Files
lib/livebook_web/plugs/user_plug.ex
defmodule LivebookWeb.UserPlug do
# Initializes the session and cookies with user-related info.
#
# The first time someone visits Livebook
# this plug stores a new random user id or the ZTA user
# in the session under `:identity_data`.
#
# Additionally the cookies are checked for the presence
# of `"user_data"` and if there is none, a new user
# attributes are stored there. This makes sure
# the client-side can always access some `"user_data"`
# for `connect_params` of the socket connection.
@behaviour Plug
import Plug.Conn
import Phoenix.Controller
@impl true
def init(opts), do: opts
@impl true
def call(conn, _opts) do
conn = ensure_user_identity(conn)
if conn.halted do
conn
else
conn
|> ensure_user_data()
|> assign_user_data()
|> set_logger_metadata()
end
end
defp ensure_user_identity(conn) do
{_type, module, _key} = identity_provider(conn)
{conn, identity_data} = authenticate(module, conn, [])
cond do
conn.halted ->
conn
identity_data ->
# Ensure we have a unique ID to identify this user/session.
id = identity_data[:id] || get_session(conn, :user_id) || Livebook.Utils.random_long_id()
conn
|> assign(:identity_data, identity_data)
|> put_session(:user_id, id)
true ->
conn
|> put_status(:forbidden)
|> put_view(LivebookWeb.ErrorHTML)
|> render("403.html", %{status: 403})
|> halt()
end
end
defp ensure_user_data(conn) do
if Map.has_key?(conn.req_cookies, "lb_user_data") do
conn
else
encoded =
%{"name" => nil, "hex_color" => Livebook.EctoTypes.HexColor.random()}
|> JSON.encode!()
|> Base.encode64()
# We disable HttpOnly, so that it can be accessed on the client
# and set expiration to 5 years
opts = [http_only: false, max_age: 157_680_000] ++ LivebookWeb.Endpoint.cookie_options()
put_resp_cookie(conn, "lb_user_data", encoded, opts)
end
end
# Copies user_data from cookie to assigns, which we later copy into
# LV session
defp assign_user_data(conn) do
user_data = conn.cookies["lb_user_data"] |> Base.decode64!() |> JSON.decode!()
assign(conn, :user_data, user_data)
end
defp set_logger_metadata(conn) do
session = get_session(conn)
%{identity_data: identity_data, user_data: user_data} = conn.assigns
current_user = build_current_user(session, identity_data, user_data)
Logger.metadata(Livebook.Utils.logger_users_metadata([current_user]))
conn
end
@doc """
Builds `Livebook.Users.User` using information from connection and
the session.
We accept individual arguments, because this is used both in plug
and LV hooks.
"""
def build_current_user(%{} = session, %{} = identity_data, %{} = user_data) do
identity_data = Map.new(identity_data, fn {k, v} -> {Atom.to_string(k), v} end)
attrs =
case Map.merge(user_data, identity_data) do
%{"name" => nil, "email" => email} = attrs -> %{attrs | "name" => email}
attrs -> attrs
end
user = Livebook.Users.User.new(session["user_id"])
case Livebook.Users.update_user(user, attrs) do
{:ok, user} -> user
{:error, _changeset} -> user
end
end
@doc """
Returns fields to be merged into the LV session.
"""
def extra_lv_session(conn) do
# These attributes are always retrieved in UserPlug, so we don't
# need to store them in the session. We need to pass them to LV,
# so we copy the assigns into LV session. This is particularly
# important for identity data, which can be huge and may exceed
# cookie limit, if it was stored in the session.
%{
"identity_data" => conn.assigns.identity_data,
"user_data" => conn.assigns.user_data
}
end
@doc """
Returns the identity provider configuration for the given `conn` or
`session`.
This mirrors `Livebook.Config.identity_provider/0`, except the it can
be overridden in tests, for each connection.
"""
@spec identity_provider(Plug.Conn.t() | map()) :: {atom(), module, binary}
if Mix.env() == :test do
def identity_provider(%Plug.Conn{} = conn) do
session = get_session(conn)
session["identity_provider_test_override"] || Livebook.Config.identity_provider()
end
else
def identity_provider(_), do: Livebook.Config.identity_provider()
end
@zta_name LivebookWeb.ZTA
@spec authenticate(module(), Plug.Conn.t() | map(), keyword()) ::
{Plug.Conn.t(), NimbleZTA.metadata()}
if Mix.env() == :test do
def authenticate(module, %Plug.Conn{} = conn, opts) do
session = get_session(conn)
name = session["zta_name_test_override"] || @zta_name
module.authenticate(name, conn, opts)
end
else
def authenticate(module, conn, opts), do: module.authenticate(@zta_name, conn, opts)
end
end