From 99b8daed7b2f79d82fcd9e89338d41b0e1564aa7 Mon Sep 17 00:00:00 2001 From: Peter Hartman Date: Sat, 9 Mar 2024 19:58:16 +0000 Subject: [PATCH] improvement: Add :csp_nonce_assign_key to ash_admin options (fix for /issues/91) (#92) --- README.md | 8 ++++++- lib/ash_admin/components/layouts.ex | 28 ++++++++++++++++++---- lib/ash_admin/router.ex | 32 +++++++++++++++++++++++-- test/page_live_test.exs | 37 +++++++++++++++++++++++++++++ test/support/router.ex | 8 +++++++ 5 files changed, 105 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 768ca92..1681a24 100644 --- a/README.md +++ b/README.md @@ -87,12 +87,18 @@ plug :put_secure_browser_headers, %{"content-security-policy" => "default-src 's in your router, then all of the styles and JavaScript used to power AshAdmin will be blocked by your browser. -To avoid this, you can add the specific AshAdmin nonces to the `default-src` allowlist, ie. +To avoid this, you can add the default AshAdmin nonces to the `default-src` allowlist, ie. ```elixir plug :put_secure_browser_headers, %{"content-security-policy" => "default-src 'nonce-ash_admin-Ed55GFnX' 'self'"} ``` +alternatively you can supply your own nonces to the `ash_admin` route by setting a `:csp_nonce_assign_key` in the options list, ie. + +```elixir +ash_admin "/admin", csp_nonce_assign_key: :csp_nonce_value +``` + This will allow AshAdmin-generated inline CSS and JS blocks to execute normally. ## Configuration diff --git a/lib/ash_admin/components/layouts.ex b/lib/ash_admin/components/layouts.ex index 0ca8ae2..16ea7dd 100644 --- a/lib/ash_admin/components/layouts.ex +++ b/lib/ash_admin/components/layouts.ex @@ -25,24 +25,32 @@ defmodule AshAdmin.Layouts do <%= assigns[:page_title] || "Ash Admin" %> - - - - <%= @inner_content %> - @@ -58,4 +66,14 @@ defmodule AshAdmin.Layouts do def live_socket_path(conn) do [Enum.map(conn.script_name, &["/" | &1]) | conn.private.live_socket_path] end + + defp csp_nonce(conn, type) when type in [:script, :style, :img] do + csp_nonce_value = conn.private.ash_admin_csp_nonce[type] + + case csp_nonce_value do + key when is_atom(key) -> conn.assigns[csp_nonce_value] + key when is_bitstring(key) -> csp_nonce_value + _ -> raise("Unexpected type of :csp_nonce_assign_key") + end + end end diff --git a/lib/ash_admin/router.ex b/lib/ash_admin/router.ex index 11eb7b3..5e0144c 100644 --- a/lib/ash_admin/router.ex +++ b/lib/ash_admin/router.ex @@ -43,6 +43,23 @@ defmodule AshAdmin.Router do Defines an AshAdmin route. It expects the `path` the admin dashboard will be mounted at and a set of options. + + ## Options + + * `:live_socket_path` - Optional override for the socket path. it must match + the `socket "/live", Phoenix.LiveView.Socket` in your endpoint. Defaults to `/live`. + + * `:on_mount` - Optional list of hooks to attach to the mount lifecycle. + + * `:session` - Optional extra session map or MFA tuple to be merged with the session. + + * `:csp_nonce_assign_key` - Optional assign key to find the CSP nonce value used for assets + Supports either `atom()` or + `%{optional(:img) => atom(), optional(:script) => atom(), optional(:style) => atom()}` + Defaults to `ash_admin-Ed55GFnX` for backwards compatibility. + + * `:live_session_name` - Optional atom to name the `live_session`. Defaults to `:ash_admin`. + ## Examples defmodule MyAppWeb.Router do use Phoenix.Router @@ -55,6 +72,7 @@ defmodule AshAdmin.Router do pipe_through [:browser] ash_admin "/admin" + ash_admin "/csp/admin", live_session_name: :ash_admin_csp, csp_nonce_assign_key: :csp_nonce_value end end """ @@ -63,7 +81,14 @@ defmodule AshAdmin.Router do import Phoenix.LiveView.Router live_socket_path = Keyword.get(opts, :live_socket_path, "/live") - live_session :ash_admin, + csp_nonce_assign_key = + case opts[:csp_nonce_assign_key] do + nil -> %{img: "ash_admin-Ed55GFnX", style: "ash_admin-Ed55GFnX", script: "ash_admin-Ed55GFnX"} + key when is_atom(key) -> %{img: key, style: key, script: key} + %{} = keys -> Map.take(keys, [:img, :style, :script]) + end + + live_session opts[:live_session_name] || :ash_admin, on_mount: List.wrap(opts[:on_mount]), session: {AshAdmin.Router, :__session__, [%{"prefix" => path}, List.wrap(opts[:session])]}, @@ -72,7 +97,10 @@ defmodule AshAdmin.Router do "#{path}/*route", AshAdmin.PageLive, :page, - private: %{live_socket_path: live_socket_path} + private: %{ + live_socket_path: live_socket_path, + ash_admin_csp_nonce: csp_nonce_assign_key + } ) end end diff --git a/test/page_live_test.exs b/test/page_live_test.exs index 3490686..303eca5 100644 --- a/test/page_live_test.exs +++ b/test/page_live_test.exs @@ -1,6 +1,7 @@ defmodule AshAdmin.Test.PageLiveTest do use ExUnit.Case, async: false + import Plug.Conn import Phoenix.ConnTest import Phoenix.LiveViewTest @endpoint AshAdmin.Test.Endpoint @@ -22,4 +23,40 @@ defmodule AshAdmin.Test.PageLiveTest do assert html =~ "body" assert html =~ "String" end + + test "embeds default csp nonces" do + html = + build_conn() + |> get("/api/admin") + |> html_response(200) + + assert html =~ "ash_admin-Ed55GFnX" + assert html =~ ~s|