mirror of
https://github.com/ash-project/ash_admin.git
synced 2024-09-19 12:53:28 +12:00
improvement: Add :csp_nonce_assign_key to ash_admin options (fix for /issues/91) (#92)
This commit is contained in:
parent
bebfea66a4
commit
99b8daed7b
5 changed files with 105 additions and 8 deletions
|
@ -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
|
||||
|
|
|
@ -25,24 +25,32 @@ defmodule AshAdmin.Layouts do
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
|
||||
<meta name="csrf-token" content={get_csrf_token()} />
|
||||
<title><%= assigns[:page_title] || "Ash Admin" %></title>
|
||||
<style nonce="ash_admin-Ed55GFnX">
|
||||
<style nonce={csp_nonce(@conn, :style)}>
|
||||
<%= raw(render("app.css", %{})) %>
|
||||
</style>
|
||||
<link
|
||||
nonce={csp_nonce(@conn, :style)}
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/jsoneditor/9.5.1/jsoneditor.min.css"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsoneditor/9.5.1/jsoneditor.min.js">
|
||||
<script
|
||||
nonce={csp_nonce(@conn, :script)}
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/jsoneditor/9.5.1/jsoneditor.min.js"
|
||||
>
|
||||
</script>
|
||||
<link rel="stylesheet" href="https://unpkg.com/easymde/dist/easymde.min.css" />
|
||||
<script src="https://unpkg.com/easymde/dist/easymde.min.js">
|
||||
<link
|
||||
nonce={csp_nonce(@conn, :style)}
|
||||
rel="stylesheet"
|
||||
href="https://unpkg.com/easymde/dist/easymde.min.css"
|
||||
/>
|
||||
<script nonce={csp_nonce(@conn, :script)} src="https://unpkg.com/easymde/dist/easymde.min.js">
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<%= @inner_content %>
|
||||
</body>
|
||||
<script nonce="ash_admin-Ed55GFnX">
|
||||
<script nonce={csp_nonce(@conn, :script)}>
|
||||
<%= raw(render("app.js", %{})) %>
|
||||
</script>
|
||||
</html>
|
||||
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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|<script nonce="ash_admin-Ed55GFnX"|
|
||||
assert html =~ ~s|<style nonce="ash_admin-Ed55GFnX"|
|
||||
end
|
||||
|
||||
test "embeds user selected csp nonces" do
|
||||
html =
|
||||
build_conn()
|
||||
|> assign(:csp_nonce_value, "csp_nonce")
|
||||
|> get("/api/csp/admin")
|
||||
|> html_response(200)
|
||||
|
||||
assert html =~ ~s|<script nonce="csp_nonce"|
|
||||
assert html =~ ~s|<style nonce="csp_nonce"|
|
||||
assert html =~ ~s|<link nonce="csp_nonce"|
|
||||
refute html =~ "ash_admin-Ed55GFnX"
|
||||
|
||||
html =
|
||||
build_conn()
|
||||
|> assign(:script_csp_nonce, "script_nonce")
|
||||
|> assign(:style_csp_nonce, "style_nonce")
|
||||
|> get("/api/csp-full/admin")
|
||||
|> html_response(200)
|
||||
|
||||
assert html =~ ~s|<script nonce="script_nonce"|
|
||||
assert html =~ ~s|<style nonce="style_nonce"|
|
||||
assert html =~ ~s|<link nonce="style_nonce"|
|
||||
refute html =~ "ash_admin-Ed55GFnX"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,6 +10,14 @@ defmodule AshAdmin.Test.Router do
|
|||
pipe_through(:browser)
|
||||
import AshAdmin.Router
|
||||
|
||||
csp_full = %{
|
||||
img: :img_csp_nonce,
|
||||
style: :style_csp_nonce,
|
||||
script: :script_csp_nonce
|
||||
}
|
||||
|
||||
ash_admin("/admin")
|
||||
ash_admin("/csp/admin", live_session_name: :ash_admin_csp, csp_nonce_assign_key: :csp_nonce_value)
|
||||
ash_admin("/csp-full/admin", live_session_name: :ash_admin_csp_full, csp_nonce_assign_key: csp_full)
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue