Make site param configurable for AuthorizeSiteAccess plug

This commit is contained in:
Adrian Gruntkowski 2024-09-19 11:43:34 +02:00
parent 5766478887
commit 8cc3a09deb
8 changed files with 117 additions and 39 deletions

View File

@ -8,7 +8,10 @@ defmodule PlausibleWeb.GoogleAnalyticsController do
plug(PlausibleWeb.RequireAccountPlug) plug(PlausibleWeb.RequireAccountPlug)
plug(PlausibleWeb.Plugs.AuthorizeSiteAccess, [:owner, :admin, :super_admin]) plug(PlausibleWeb.Plugs.AuthorizeSiteAccess,
site_param: {:path, :website},
allowed_roles: [:owner, :admin, :super_admin]
)
def property_form( def property_form(
conn, conn,

View File

@ -4,7 +4,11 @@ defmodule PlausibleWeb.InvitationController do
plug PlausibleWeb.RequireAccountPlug plug PlausibleWeb.RequireAccountPlug
plug PlausibleWeb.Plugs.AuthorizeSiteAccess, plug PlausibleWeb.Plugs.AuthorizeSiteAccess,
[:owner, :admin] when action in [:remove_invitation] [
site_param: {:path, :website},
allowed_roles: [:owner, :admin]
]
when action in [:remove_invitation]
def accept_invitation(conn, %{"invitation_id" => invitation_id}) do def accept_invitation(conn, %{"invitation_id" => invitation_id}) do
case Plausible.Site.Memberships.accept_invitation(invitation_id, conn.assigns.current_user) do case Plausible.Site.Memberships.accept_invitation(invitation_id, conn.assigns.current_user) do

View File

@ -18,10 +18,20 @@ defmodule PlausibleWeb.Site.MembershipController do
@only_owner_is_allowed_to [:transfer_ownership_form, :transfer_ownership] @only_owner_is_allowed_to [:transfer_ownership_form, :transfer_ownership]
plug PlausibleWeb.RequireAccountPlug plug PlausibleWeb.RequireAccountPlug
plug PlausibleWeb.Plugs.AuthorizeSiteAccess, [:owner] when action in @only_owner_is_allowed_to
plug PlausibleWeb.Plugs.AuthorizeSiteAccess, plug PlausibleWeb.Plugs.AuthorizeSiteAccess,
[:owner, :admin] when action not in @only_owner_is_allowed_to [
site_param: {:path, :website},
allowed_roles: [:owner]
]
when action in @only_owner_is_allowed_to
plug PlausibleWeb.Plugs.AuthorizeSiteAccess,
[
site_param: {:path, :website},
allowed_roles: [:owner, :admin]
]
when action not in @only_owner_is_allowed_to
def invite_member_form(conn, _params) do def invite_member_form(conn, _params) do
site = site =

View File

@ -6,12 +6,14 @@ defmodule PlausibleWeb.SiteController do
alias Plausible.Sites alias Plausible.Sites
alias Plausible.Billing.Quota alias Plausible.Billing.Quota
plug(PlausibleWeb.RequireAccountPlug) plug PlausibleWeb.RequireAccountPlug
plug( plug PlausibleWeb.Plugs.AuthorizeSiteAccess,
PlausibleWeb.Plugs.AuthorizeSiteAccess, [
[:owner, :admin, :super_admin] when action not in [:new, :create_site] site_param: {:path, :website},
) allowed_roles: [:owner, :admin, :super_admin]
]
when action not in [:new, :create_site]
def new(conn, params) do def new(conn, params) do
flow = params["flow"] || PlausibleWeb.Flows.register() flow = params["flow"] || PlausibleWeb.Flows.register()

View File

@ -48,7 +48,8 @@ defmodule PlausibleWeb.StatsController do
alias Plausible.Stats.{Filters, Query} alias Plausible.Stats.{Filters, Query}
alias PlausibleWeb.Api alias PlausibleWeb.Api
plug(PlausibleWeb.Plugs.AuthorizeSiteAccess when action in [:stats, :csv_export]) plug PlausibleWeb.Plugs.AuthorizeSiteAccess,
[site_param: {:path, :domain}] when action in [:stats, :csv_export]
def stats(%{assigns: %{site: site}} = conn, _params) do def stats(%{assigns: %{site: site}} = conn, _params) do
site = Plausible.Repo.preload(site, :owner) site = Plausible.Repo.preload(site, :owner)

View File

@ -10,22 +10,27 @@ defmodule PlausibleWeb.Plugs.AuthorizeSiteAccess do
@all_roles [:public, :viewer, :admin, :super_admin, :owner] @all_roles [:public, :viewer, :admin, :super_admin, :owner]
def init([]), do: @all_roles def all_roles(), do: @all_roles
def init(allowed_roles) do def init(opts) do
allowed_roles = Keyword.get(opts, :allowed_roles, @all_roles)
site_param = Keyword.fetch!(opts, :site_param)
unknown_roles = allowed_roles -- @all_roles unknown_roles = allowed_roles -- @all_roles
if unknown_roles != [] do if unknown_roles != [] do
raise ArgumentError, "Unknown allowed roles configured: #{inspect(unknown_roles)}" raise ArgumentError, "Unknown allowed roles configured: #{inspect(unknown_roles)}"
end end
allowed_roles %{site_param: site_param, allowed_roles: allowed_roles}
end end
def call(conn, allowed_roles) do def call(conn, %{site_param: site_param, allowed_roles: allowed_roles}) do
current_user = conn.assigns[:current_user] current_user = conn.assigns[:current_user]
with {:ok, %{site: site, role: membership_role}} <- get_site_with_role(conn, current_user), domain = get_domain(conn, site_param)
with {:ok, %{site: site, role: membership_role}} <-
get_site_with_role(conn, current_user, domain),
{:ok, shared_link} <- maybe_get_shared_link(conn, site) do {:ok, shared_link} <- maybe_get_shared_link(conn, site) do
role = role =
cond do cond do
@ -63,13 +68,19 @@ defmodule PlausibleWeb.Plugs.AuthorizeSiteAccess do
end end
end end
defp get_site_with_role(conn, current_user) do defp get_domain(conn, {:path, param}) when is_atom(param) do
# addition is flimsy, do we need an extra argument on plug init to control where we look for the domain? conn.path_params[Atom.to_string(param)]
domain = end
conn.path_params["domain"] || conn.path_params["website"] ||
(conn.method == "POST" && conn.path_info == ["api", "docs", "query"] &&
conn.params["site_id"])
defp get_domain(conn, param) when is_atom(param) do
conn.params[Atom.to_string(param)]
end
defp get_domain(_conn, _) do
raise ArgumentError, "site_param must be either {:path, param_atom} or param_atom"
end
defp get_site_with_role(conn, current_user, domain) when is_binary(domain) do
site_query = site_query =
from( from(
s in Plausible.Site, s in Plausible.Site,
@ -96,6 +107,10 @@ defmodule PlausibleWeb.Plugs.AuthorizeSiteAccess do
end end
end end
defp get_site_with_role(conn, _current_user, _domain) do
error_not_found(conn)
end
defp maybe_get_shared_link(conn, site) do defp maybe_get_shared_link(conn, site) do
slug = conn.path_params["slug"] || conn.params["auth"] slug = conn.path_params["slug"] || conn.params["auth"]

View File

@ -42,7 +42,7 @@ defmodule PlausibleWeb.Router do
plug :accepts, ["json"] plug :accepts, ["json"]
plug :fetch_session plug :fetch_session
plug PlausibleWeb.AuthPlug plug PlausibleWeb.AuthPlug
plug PlausibleWeb.Plugs.AuthorizeSiteAccess plug PlausibleWeb.Plugs.AuthorizeSiteAccess, site_param: {:path, :domain}
plug PlausibleWeb.Plugs.NoRobots plug PlausibleWeb.Plugs.NoRobots
end end
@ -50,7 +50,11 @@ defmodule PlausibleWeb.Router do
plug :accepts, ["json"] plug :accepts, ["json"]
plug :fetch_session plug :fetch_session
plug PlausibleWeb.AuthPlug plug PlausibleWeb.AuthPlug
plug PlausibleWeb.Plugs.AuthorizeSiteAccess, [:admin, :super_admin, :owner]
plug PlausibleWeb.Plugs.AuthorizeSiteAccess,
site_param: :site_id,
allowed_roles: [:admin, :super_admin, :owner]
plug PlausibleWeb.Plugs.NoRobots plug PlausibleWeb.Plugs.NoRobots
end end

View File

@ -6,7 +6,19 @@ defmodule PlausibleWeb.Plugs.AuthorizeSiteAccessTest do
test "init rejects invalid role names" do test "init rejects invalid role names" do
assert_raise ArgumentError, fn -> assert_raise ArgumentError, fn ->
AuthorizeSiteAccess.init(_allowed_roles = [:admin, :invalid]) AuthorizeSiteAccess.init(site_param: :domain, allowed_roles: [:admin, :invalid])
end
end
test "init rejects invalid site_param format" do
assert_raise ArgumentError, fn ->
AuthorizeSiteAccess.init(site_param: "wrong", allowed_roles: [:admin, :invalid])
end
end
test "init crashes on missing site param" do
assert_raise KeyError, fn ->
AuthorizeSiteAccess.init(allowed_roles: [:admin])
end end
end end
@ -15,7 +27,10 @@ defmodule PlausibleWeb.Plugs.AuthorizeSiteAccessTest do
conn conn
|> bypass_through(PlausibleWeb.Router) |> bypass_through(PlausibleWeb.Router)
|> get("/invalid.domain") |> get("/invalid.domain")
|> AuthorizeSiteAccess.call(_all_allowed_roles = []) |> AuthorizeSiteAccess.call(%{
site_param: {:path, :domain},
allowed_roles: [:admin, :owner]
})
assert conn.halted assert conn.halted
assert html_response(conn, 404) assert html_response(conn, 404)
@ -28,7 +43,10 @@ defmodule PlausibleWeb.Plugs.AuthorizeSiteAccessTest do
conn conn
|> bypass_through(PlausibleWeb.Router) |> bypass_through(PlausibleWeb.Router)
|> get("/#{site.domain}") |> get("/#{site.domain}")
|> AuthorizeSiteAccess.call(_all_allowed_roles = []) |> AuthorizeSiteAccess.call(%{
site_param: {:path, :domain},
allowed_roles: [:admin, :owner]
})
assert conn.halted assert conn.halted
assert html_response(conn, 404) assert html_response(conn, 404)
@ -47,7 +65,10 @@ defmodule PlausibleWeb.Plugs.AuthorizeSiteAccessTest do
"domain" => site.domain, "domain" => site.domain,
"website" => site.domain "website" => site.domain
}) })
|> AuthorizeSiteAccess.call(_allowed_roles = [:admin, :owner]) |> AuthorizeSiteAccess.call(%{
site_param: {:path, :domain},
allowed_roles: [:admin, :owner]
})
assert conn.halted assert conn.halted
assert html_response(conn, 404) assert html_response(conn, 404)
@ -67,7 +88,10 @@ defmodule PlausibleWeb.Plugs.AuthorizeSiteAccessTest do
"domain" => site.domain, "domain" => site.domain,
"website" => site.domain "website" => site.domain
}) })
|> AuthorizeSiteAccess.call(_allowed_roles = [:admin, :owner]) |> AuthorizeSiteAccess.call(%{
site_param: {:path, :website},
allowed_roles: [:admin, :owner]
})
assert conn.halted assert conn.halted
assert conn.status == 404 assert conn.status == 404
@ -81,7 +105,10 @@ defmodule PlausibleWeb.Plugs.AuthorizeSiteAccessTest do
conn conn
|> bypass_through(PlausibleWeb.Router) |> bypass_through(PlausibleWeb.Router)
|> get("/api/stats/#{site.domain}/main-graph") |> get("/api/stats/#{site.domain}/main-graph")
|> AuthorizeSiteAccess.call([:admin, :owner, :super_admin]) |> AuthorizeSiteAccess.call(%{
site_param: {:path, :domain},
allowed_roles: [:admin, :owner, :super_admin]
})
assert conn.halted assert conn.halted
@ -102,7 +129,10 @@ defmodule PlausibleWeb.Plugs.AuthorizeSiteAccessTest do
conn conn
|> bypass_through(PlausibleWeb.Router) |> bypass_through(PlausibleWeb.Router)
|> put("/sites/#{site.domain}/shared-links/#{shared_link_other_site.slug}", params) |> put("/sites/#{site.domain}/shared-links/#{shared_link_other_site.slug}", params)
|> AuthorizeSiteAccess.call(_allowed_roles = [:super_admin, :admin, :owner]) |> AuthorizeSiteAccess.call(%{
site_param: {:path, :website},
allowed_roles: [:super_admin, :admin, :owner]
})
assert conn.halted assert conn.halted
assert conn.status == 404 assert conn.status == 404
@ -121,7 +151,10 @@ defmodule PlausibleWeb.Plugs.AuthorizeSiteAccessTest do
conn conn
|> bypass_through(PlausibleWeb.Router) |> bypass_through(PlausibleWeb.Router)
|> get("/#{other_site.domain}", %{"auth" => shared_link.slug}) |> get("/#{other_site.domain}", %{"auth" => shared_link.slug})
|> AuthorizeSiteAccess.call(_all_allowed_roles = []) |> AuthorizeSiteAccess.call(%{
site_param: {:path, :domain},
allowed_roles: AuthorizeSiteAccess.all_roles()
})
assert conn.halted assert conn.halted
assert conn.status == 404 assert conn.status == 404
@ -144,7 +177,10 @@ defmodule PlausibleWeb.Plugs.AuthorizeSiteAccessTest do
conn conn
|> bypass_through(PlausibleWeb.Router) |> bypass_through(PlausibleWeb.Router)
|> put("/sites/#{site.domain}/shared-links/#{shared_link_other_site.slug}", params) |> put("/sites/#{site.domain}/shared-links/#{shared_link_other_site.slug}", params)
|> AuthorizeSiteAccess.call(_allowed_roles = [:super_admin, :admin, :owner]) |> AuthorizeSiteAccess.call(%{
site_param: {:path, :website},
allowed_roles: [:super_admin, :admin, :owner]
})
assert conn.halted assert conn.halted
assert conn.status == 404 assert conn.status == 404
@ -158,7 +194,7 @@ defmodule PlausibleWeb.Plugs.AuthorizeSiteAccessTest do
conn conn
|> bypass_through(PlausibleWeb.Router) |> bypass_through(PlausibleWeb.Router)
|> get("/#{site.domain}") |> get("/#{site.domain}")
|> AuthorizeSiteAccess.call(_all_allowed_roles = [:owner]) |> AuthorizeSiteAccess.call(%{site_param: {:path, :domain}, allowed_roles: [:owner]})
assert conn.halted assert conn.halted
assert html_response(conn, 404) assert html_response(conn, 404)
@ -173,7 +209,10 @@ defmodule PlausibleWeb.Plugs.AuthorizeSiteAccessTest do
conn conn
|> bypass_through(PlausibleWeb.Router) |> bypass_through(PlausibleWeb.Router)
|> get("/#{site.domain}") |> get("/#{site.domain}")
|> AuthorizeSiteAccess.call(_all_allowed_roles = [unquote(role)]) |> AuthorizeSiteAccess.call(%{
site_param: {:path, :domain},
allowed_roles: [unquote(role)]
})
refute conn.halted refute conn.halted
assert conn.assigns.site.id == site.id assert conn.assigns.site.id == site.id
@ -191,7 +230,7 @@ defmodule PlausibleWeb.Plugs.AuthorizeSiteAccessTest do
conn conn
|> bypass_through(PlausibleWeb.Router) |> bypass_through(PlausibleWeb.Router)
|> get("/#{site.domain}") |> get("/#{site.domain}")
|> AuthorizeSiteAccess.call(_all_allowed_roles = [:super_admin]) |> AuthorizeSiteAccess.call(%{site_param: {:path, :domain}, allowed_roles: [:super_admin]})
refute conn.halted refute conn.halted
assert conn.assigns.site.id == site.id assert conn.assigns.site.id == site.id
@ -205,7 +244,7 @@ defmodule PlausibleWeb.Plugs.AuthorizeSiteAccessTest do
conn conn
|> bypass_through(PlausibleWeb.Router) |> bypass_through(PlausibleWeb.Router)
|> get("/#{site.domain}") |> get("/#{site.domain}")
|> AuthorizeSiteAccess.call(_all_allowed_roles = [:public]) |> AuthorizeSiteAccess.call(%{site_param: {:path, :domain}, allowed_roles: [:public]})
refute conn.halted refute conn.halted
assert conn.assigns.site.id == site.id assert conn.assigns.site.id == site.id
@ -218,7 +257,7 @@ defmodule PlausibleWeb.Plugs.AuthorizeSiteAccessTest do
build_conn() build_conn()
|> bypass_through(PlausibleWeb.Router) |> bypass_through(PlausibleWeb.Router)
|> get("/#{site.domain}") |> get("/#{site.domain}")
|> AuthorizeSiteAccess.call(_all_allowed_roles = [:public]) |> AuthorizeSiteAccess.call(%{site_param: {:path, :domain}, allowed_roles: [:public]})
refute conn.halted refute conn.halted
assert conn.assigns.site.id == site.id assert conn.assigns.site.id == site.id
@ -232,7 +271,7 @@ defmodule PlausibleWeb.Plugs.AuthorizeSiteAccessTest do
conn conn
|> bypass_through(PlausibleWeb.Router) |> bypass_through(PlausibleWeb.Router)
|> get("/#{site.domain}", %{"auth" => shared_link.slug}) |> get("/#{site.domain}", %{"auth" => shared_link.slug})
|> AuthorizeSiteAccess.call(_all_allowed_roles = [:public]) |> AuthorizeSiteAccess.call(%{site_param: {:path, :domain}, allowed_roles: [:public]})
refute conn.halted refute conn.halted
assert conn.assigns.site.id == site.id assert conn.assigns.site.id == site.id
@ -246,7 +285,7 @@ defmodule PlausibleWeb.Plugs.AuthorizeSiteAccessTest do
build_conn() build_conn()
|> bypass_through(PlausibleWeb.Router) |> bypass_through(PlausibleWeb.Router)
|> get("/#{site.domain}", %{"auth" => shared_link.slug}) |> get("/#{site.domain}", %{"auth" => shared_link.slug})
|> AuthorizeSiteAccess.call(_all_allowed_roles = [:public]) |> AuthorizeSiteAccess.call(%{site_param: {:path, :domain}, allowed_roles: [:public]})
refute conn.halted refute conn.halted
assert conn.assigns.site.id == site.id assert conn.assigns.site.id == site.id