From 8f20daab3c087555a4d8e9a90ae1171500a93264 Mon Sep 17 00:00:00 2001 From: Ho-Yon Mak Date: Thu, 4 Apr 2024 16:58:15 +0100 Subject: [PATCH] Allow passing csp_nonce_assign_key into router --- README.md | 20 ++++ lib/fun_with_flags/ui/router.ex | 15 ++- lib/fun_with_flags/ui/templates.ex | 5 + .../ui/templates/_head.html.eex | 4 +- .../ui/templates/details.html.eex | 2 +- test/fun_with_flags/ui/templates_test.exs | 99 ++++++++++++++----- 6 files changed, 117 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 7481047..c23f6fa 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,26 @@ defmodule MyPhoenixAppWeb.Router do end ``` +### Content Security Policy + +Content security policy nonces can be passed into the router to allow usage of strict Content Security Policies throughout an application. + +This can be achieved by passing in a `csp_nonce_assign_key` to the `FunWithFlags.UI.Router` forward. Values for the nonces should be set in the Conn assigns before reaching this router. + +This an either be a single nonce value, or separate values for script and style tags. + +For example: + +``` elixir +forward "/", FunWithFlags.UI.Router, namespace: "feature-flags", csp_nonce_assign_key: :my_csp_nonce +``` + +Or: + +``` elixir +forward "/", FunWithFlags.UI.Router, namespace: "feature-flags", csp_nonce_assign_key: %{style: :my_style_nonce, script: :my_script_nonce} +``` + ## Caveats While the base `fun_with_flags` library is quite relaxed in terms of valid flag names, group names and actor identifers, this web dashboard extension applies some more restrictive rules. diff --git a/lib/fun_with_flags/ui/router.ex b/lib/fun_with_flags/ui/router.ex index 3251925..61413f8 100644 --- a/lib/fun_with_flags/ui/router.ex +++ b/lib/fun_with_flags/ui/router.ex @@ -30,7 +30,10 @@ defmodule FunWithFlags.UI.Router do @doc false def call(conn, opts) do - conn = extract_namespace(conn, opts) + conn = + conn + |> extract_namespace(opts) + |> extract_csp_nonce_key(opts) super(conn, opts) end @@ -286,6 +289,16 @@ defmodule FunWithFlags.UI.Router do Plug.Conn.assign(conn, :namespace, "/" <> ns) end + defp extract_csp_nonce_key(conn, opts) do + csp_nonce_assign_key = + case opts[:csp_nonce_assign_key] do + nil -> nil + key when is_atom(key) -> %{style: key, script: key} + %{} = keys -> Map.take(keys, [:style, :script]) + end + + Plug.Conn.put_private(conn, :csp_nonce_assign_key, csp_nonce_assign_key) + end defp assign_csrf_token(conn, _opts) do csrf_token = Plug.CSRFProtection.get_csrf_token() diff --git a/lib/fun_with_flags/ui/templates.ex b/lib/fun_with_flags/ui/templates.ex index 8b0f6cb..81932d1 100644 --- a/lib/fun_with_flags/ui/templates.ex +++ b/lib/fun_with_flags/ui/templates.ex @@ -82,4 +82,9 @@ defmodule FunWithFlags.UI.Templates do |> to_string() |> URI.encode() end + + def csp_nonce(conn, type) do + csp_nonce_assign_key = conn.private.csp_nonce_assign_key[type] + conn.assigns[csp_nonce_assign_key] + end end diff --git a/lib/fun_with_flags/ui/templates/_head.html.eex b/lib/fun_with_flags/ui/templates/_head.html.eex index 3422e90..80ec632 100644 --- a/lib/fun_with_flags/ui/templates/_head.html.eex +++ b/lib/fun_with_flags/ui/templates/_head.html.eex @@ -2,6 +2,6 @@ FunWithFlags - <%= @title %> - "> - "> + "> + "> diff --git a/lib/fun_with_flags/ui/templates/details.html.eex b/lib/fun_with_flags/ui/templates/details.html.eex index 04e0bb6..df9a35d 100644 --- a/lib/fun_with_flags/ui/templates/details.html.eex +++ b/lib/fun_with_flags/ui/templates/details.html.eex @@ -140,6 +140,6 @@ - + diff --git a/test/fun_with_flags/ui/templates_test.exs b/test/fun_with_flags/ui/templates_test.exs index de31887..3802e73 100644 --- a/test/fun_with_flags/ui/templates_test.exs +++ b/test/fun_with_flags/ui/templates_test.exs @@ -7,17 +7,20 @@ defmodule FunWithFlags.UI.TemplatesTest do import FunWithFlags.UI.TestUtils setup_all do - on_exit(__MODULE__, fn() -> clear_redis_test_db() end) + on_exit(__MODULE__, fn -> clear_redis_test_db() end) :ok end setup do - conn = Plug.Conn.assign(%Plug.Conn{}, :namespace, "/pear") - conn = Plug.Conn.assign(conn, :csrf_token, Plug.CSRFProtection.get_csrf_token()) + conn = + %Plug.Conn{} + |> Plug.Conn.assign(:namespace, "/pear") + |> Plug.Conn.put_private(:csp_nonce_assign_key, %{style: :style_key, script: :script_key}) + |> Plug.Conn.assign(:csrf_token, Plug.CSRFProtection.get_csrf_token()) + {:ok, conn: conn} end - describe "_head()" do test "it renders", %{conn: conn} do out = Templates._head(conn: conn, title: "Coconut") @@ -31,13 +34,13 @@ defmodule FunWithFlags.UI.TemplatesTest do end end - describe "index()" do setup do flags = [ %Flag{name: :pineapple, gates: [Gate.new(:boolean, true)]}, - %Flag{name: :papaya, gates: [Gate.new(:boolean, false)]}, + %Flag{name: :papaya, gates: [Gate.new(:boolean, false)]} ] + {:ok, flags: flags} end @@ -55,7 +58,6 @@ defmodule FunWithFlags.UI.TemplatesTest do end end - describe "details()" do setup do flag = %Flag{name: :avocado, gates: []} @@ -77,38 +79,65 @@ defmodule FunWithFlags.UI.TemplatesTest do test "it includes the CSRF token", %{conn: conn, flag: flag} do csrf_token = Plug.CSRFProtection.get_csrf_token() out = Templates.details(conn: conn, flag: flag) - assert String.contains?(out, ~s{}) + + assert String.contains?( + out, + ~s{} + ) end - test "it includes the global toggle, the new actor and new group forms, and the global delete form", %{conn: conn, flag: flag} do + test "it includes the global toggle, the new actor and new group forms, and the global delete form", + %{conn: conn, flag: flag} do out = Templates.details(conn: conn, flag: flag) - assert String.contains?(out, ~s{
}) + + assert String.contains?( + out, + ~s{} + ) end - test "with no boolean gate, it includes both the enabled and disable boolean buttons", %{conn: conn, flag: flag} do + test "with no boolean gate, it includes both the enabled and disable boolean buttons", %{ + conn: conn, + flag: flag + } do out = Templates.details(conn: conn, flag: flag) assert String.contains?(out, ~s{