mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-06 04:54:29 +08:00
163 lines
4.8 KiB
Elixir
163 lines
4.8 KiB
Elixir
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(), Livebook.ZTA.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 identity_provider(module, conn, opts), do: module.authenticate(@zta_name, conn, opts)
|
|
end
|
|
end
|