Skip to content
31 changes: 28 additions & 3 deletions lib/plausible_web/plugs/authorize_public_api.ex
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,13 @@ defmodule PlausibleWeb.Plugs.AuthorizePublicAPI do
defp verify_by_scope(conn, api_key, "stats:read:" <> _ = scope) do
with :ok <- check_scope(api_key, scope),
{:ok, site} <- find_site(conn.params["site_id"]),
:ok <- verify_site_access(api_key, site, Plausible.Billing.Feature.StatsAPI) do
:ok <-
verify_site_access(
site: site,
api_key: api_key,
feature: Plausible.Billing.Feature.StatsAPI,
allow_consolidated_views: conn.private[:allow_consolidated_views]
) do
Plausible.OpenTelemetry.add_site_attributes(site)
site = Plausible.Repo.preload(site, :completed_imports)
{:ok, assign(conn, :site, site)}
Expand Down Expand Up @@ -193,7 +199,11 @@ defmodule PlausibleWeb.Plugs.AuthorizePublicAPI do
defp maybe_verify_site_access(conn, api_key, feature) do
case find_site(conn.params["site_id"]) do
{:ok, site} ->
verify_site_access(api_key, site, feature)
verify_site_access(
site: site,
api_key: api_key,
feature: feature
)

_ ->
:ok
Expand Down Expand Up @@ -226,13 +236,21 @@ defmodule PlausibleWeb.Plugs.AuthorizePublicAPI do
end
end

defp verify_site_access(api_key, site, feature) do
defp verify_site_access(opts) do
site = Keyword.fetch!(opts, :site)
api_key = Keyword.fetch!(opts, :api_key)
feature = Keyword.fetch!(opts, :feature)
allow_consolidated_views = Keyword.fetch!(opts, :allow_consolidated_views)

team = Repo.preload(site, :team).team

is_member? = Plausible.Teams.Memberships.site_member?(site, api_key.user)
is_super_admin? = Auth.is_super_admin?(api_key.user_id)

cond do
Plausible.Sites.consolidated?(site) && !allow_consolidated_views ->
{:error, :unavailable_for_consolidated_view}

is_super_admin? ->
:ok

Expand Down Expand Up @@ -278,6 +296,13 @@ defmodule PlausibleWeb.Plugs.AuthorizePublicAPI do
end
end

defp send_error(conn, _, {:error, :unavailable_for_consolidated_view}) do
H.bad_request(
conn,
"This operation is unavailable for a consolidated view"
)
end

defp send_error(conn, _, {:error, :missing_api_key}) do
H.unauthorized(
conn,
Expand Down
9 changes: 9 additions & 0 deletions lib/plausible_web/plugs/authorize_site_access.ex
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ defmodule PlausibleWeb.Plugs.AuthorizeSiteAccess do
with {:ok, domain} <- get_domain(conn, site_param),
{:ok, %{site: site, role: membership_role, member_type: member_type}} <-
get_site_with_role(conn, current_user, domain),
:ok <- ensure_consolidated_view_access(conn, site),
{:ok, shared_link} <- maybe_get_shared_link(conn, site) do
role =
cond do
Expand Down Expand Up @@ -188,6 +189,14 @@ defmodule PlausibleWeb.Plugs.AuthorizeSiteAccess do
end
end

defp ensure_consolidated_view_access(conn, site) do
if Plausible.Sites.consolidated?(site) && !conn.private[:allow_consolidated_views] do
error_not_found(conn)
else
:ok
end
end

defp maybe_get_shared_link(conn, site) do
slug = conn.path_params["slug"] || conn.params["auth"]

Expand Down
187 changes: 101 additions & 86 deletions lib/plausible_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -268,38 +268,41 @@ defmodule PlausibleWeb.Router do
get "/:domain/funnels/:id", StatsController, :funnel
end

get "/:domain/current-visitors", StatsController, :current_visitors
get "/:domain/main-graph", StatsController, :main_graph
get "/:domain/top-stats", StatsController, :top_stats
get "/:domain/sources", StatsController, :sources
get "/:domain/channels", StatsController, :channels
get "/:domain/utm_mediums", StatsController, :utm_mediums
get "/:domain/utm_sources", StatsController, :utm_sources
get "/:domain/utm_campaigns", StatsController, :utm_campaigns
get "/:domain/utm_contents", StatsController, :utm_contents
get "/:domain/utm_terms", StatsController, :utm_terms
get "/:domain/referrers/:referrer", StatsController, :referrer_drilldown
get "/:domain/pages", StatsController, :pages
get "/:domain/entry-pages", StatsController, :entry_pages
get "/:domain/exit-pages", StatsController, :exit_pages
get "/:domain/countries", StatsController, :countries
get "/:domain/regions", StatsController, :regions
get "/:domain/cities", StatsController, :cities
get "/:domain/browsers", StatsController, :browsers
get "/:domain/browser-versions", StatsController, :browser_versions
get "/:domain/operating-systems", StatsController, :operating_systems
get "/:domain/operating-system-versions", StatsController, :operating_system_versions
get "/:domain/screen-sizes", StatsController, :screen_sizes
get "/:domain/conversions", StatsController, :conversions
get "/:domain/custom-prop-values/:prop_key", StatsController, :custom_prop_values
get "/:domain/suggestions/:filter_name", StatsController, :filter_suggestions

get "/:domain/suggestions/custom-prop-values/:prop_key",
StatsController,
:custom_prop_value_filter_suggestions
end

scope "/:domain/segments", PlausibleWeb.Api.Internal do
scope private: %{allow_consolidated_views: true} do
get "/:domain/current-visitors", StatsController, :current_visitors
get "/:domain/main-graph", StatsController, :main_graph
get "/:domain/top-stats", StatsController, :top_stats
get "/:domain/sources", StatsController, :sources
get "/:domain/channels", StatsController, :channels
get "/:domain/utm_mediums", StatsController, :utm_mediums
get "/:domain/utm_sources", StatsController, :utm_sources
get "/:domain/utm_campaigns", StatsController, :utm_campaigns
get "/:domain/utm_contents", StatsController, :utm_contents
get "/:domain/utm_terms", StatsController, :utm_terms
get "/:domain/referrers/:referrer", StatsController, :referrer_drilldown
get "/:domain/pages", StatsController, :pages
get "/:domain/entry-pages", StatsController, :entry_pages
get "/:domain/exit-pages", StatsController, :exit_pages
get "/:domain/countries", StatsController, :countries
get "/:domain/regions", StatsController, :regions
get "/:domain/cities", StatsController, :cities
get "/:domain/browsers", StatsController, :browsers
get "/:domain/browser-versions", StatsController, :browser_versions
get "/:domain/operating-systems", StatsController, :operating_systems
get "/:domain/operating-system-versions", StatsController, :operating_system_versions
get "/:domain/screen-sizes", StatsController, :screen_sizes
get "/:domain/conversions", StatsController, :conversions
get "/:domain/custom-prop-values/:prop_key", StatsController, :custom_prop_values
get "/:domain/suggestions/:filter_name", StatsController, :filter_suggestions

get "/:domain/suggestions/custom-prop-values/:prop_key",
StatsController,
:custom_prop_value_filter_suggestions
end
end

scope "/:domain/segments", PlausibleWeb.Api.Internal,
private: %{allow_consolidated_views: true} do
post "/", SegmentsController, :create
patch "/:segment_id", SegmentsController, :update
delete "/:segment_id", SegmentsController, :delete
Expand All @@ -317,7 +320,14 @@ defmodule PlausibleWeb.Router do
end

scope "/api/v2", PlausibleWeb.Api,
assigns: %{api_scope: "stats:read:*", api_context: :site, schema_type: :public} do
private: %{
allow_consolidated_views: true
},
assigns: %{
api_scope: "stats:read:*",
api_context: :site,
schema_type: :public
} do
pipe_through [:public_api, PlausibleWeb.Plugs.AuthorizePublicAPI]

post "/query", ExternalQueryApiController, :query
Expand Down Expand Up @@ -559,44 +569,6 @@ defmodule PlausibleWeb.Router do
post "/sites", SiteController, :create_site
post "/sites/:domain/make-public", SiteController, :make_public
post "/sites/:domain/make-private", SiteController, :make_private
post "/sites/:domain/weekly-report/enable", SiteController, :enable_weekly_report
post "/sites/:domain/weekly-report/disable", SiteController, :disable_weekly_report
post "/sites/:domain/weekly-report/recipients", SiteController, :add_weekly_report_recipient

delete "/sites/:domain/weekly-report/recipients/:recipient",
SiteController,
:remove_weekly_report_recipient

post "/sites/:domain/monthly-report/enable", SiteController, :enable_monthly_report
post "/sites/:domain/monthly-report/disable", SiteController, :disable_monthly_report

post "/sites/:domain/monthly-report/recipients",
SiteController,
:add_monthly_report_recipient

delete "/sites/:domain/monthly-report/recipients/:recipient",
SiteController,
:remove_monthly_report_recipient

post "/sites/:domain/traffic-change-notification/:type/enable",
SiteController,
:enable_traffic_change_notification

post "/sites/:domain/traffic-change-notification/:type/disable",
SiteController,
:disable_traffic_change_notification

put "/sites/:domain/traffic-change-notification/:type",
SiteController,
:update_traffic_change_notification

post "/sites/:domain/traffic-change-notification/:type/recipients",
SiteController,
:add_traffic_change_notification_recipient

delete "/sites/:domain/traffic-change-notification/:type/recipients/:recipient",
SiteController,
:remove_traffic_change_notification_recipient

get "/sites/:domain/memberships/invite", Site.MembershipController, :invite_member_form
post "/sites/:domain/memberships/invite", Site.MembershipController, :invite_member
Expand All @@ -619,9 +591,6 @@ defmodule PlausibleWeb.Router do

delete "/sites/:domain/memberships/u/:id", Site.MembershipController, :remove_member_by_user

get "/sites/:domain/weekly-report/unsubscribe", UnsubscribeController, :weekly_report
get "/sites/:domain/monthly-report/unsubscribe", UnsubscribeController, :monthly_report

scope alias: Live, assigns: %{connect_live_socket: true} do
pipe_through [:app_layout, PlausibleWeb.RequireAccountPlug]

Expand All @@ -648,28 +617,18 @@ defmodule PlausibleWeb.Router do
end
end

get "/:domain/settings", SiteController, :settings
get "/:domain/settings/general", SiteController, :settings_general
get "/:domain/settings/people", SiteController, :settings_people
get "/:domain/settings/visibility", SiteController, :settings_visibility
get "/:domain/settings/goals", SiteController, :settings_goals
get "/:domain/settings/properties", SiteController, :settings_props

on_ee do
get "/:domain/settings/funnels", SiteController, :settings_funnels
end

get "/:domain/settings/email-reports", SiteController, :settings_email_reports
get "/:domain/settings/danger-zone", SiteController, :settings_danger_zone
get "/:domain/settings/integrations", SiteController, :settings_integrations
get "/:domain/settings/shields/:shield", SiteController, :settings_shields
get "/:domain/settings/imports-exports", SiteController, :settings_imports_exports

put "/:domain/settings/features/visibility/:setting",
SiteController,
:update_feature_visibility

put "/:domain/settings", SiteController, :update_settings
put "/:domain/settings/google", SiteController, :update_google_auth
delete "/:domain/settings/google-search", SiteController, :delete_google_auth
delete "/:domain/settings/google-import", SiteController, :delete_google_auth
Expand All @@ -695,7 +654,63 @@ defmodule PlausibleWeb.Router do

get "/debug/clickhouse", DebugController, :clickhouse

get "/:domain/export", StatsController, :csv_export
get "/:domain/*path", StatsController, :stats
scope private: %{allow_consolidated_views: true} do
post "/sites/:domain/weekly-report/enable", SiteController, :enable_weekly_report
post "/sites/:domain/weekly-report/disable", SiteController, :disable_weekly_report
post "/sites/:domain/weekly-report/recipients", SiteController, :add_weekly_report_recipient

delete "/sites/:domain/weekly-report/recipients/:recipient",
SiteController,
:remove_weekly_report_recipient

post "/sites/:domain/monthly-report/enable", SiteController, :enable_monthly_report
post "/sites/:domain/monthly-report/disable", SiteController, :disable_monthly_report

post "/sites/:domain/monthly-report/recipients",
SiteController,
:add_monthly_report_recipient

delete "/sites/:domain/monthly-report/recipients/:recipient",
SiteController,
:remove_monthly_report_recipient

post "/sites/:domain/traffic-change-notification/:type/enable",
SiteController,
:enable_traffic_change_notification

post "/sites/:domain/traffic-change-notification/:type/disable",
SiteController,
:disable_traffic_change_notification

put "/sites/:domain/traffic-change-notification/:type",
SiteController,
:update_traffic_change_notification

post "/sites/:domain/traffic-change-notification/:type/recipients",
SiteController,
:add_traffic_change_notification_recipient

delete "/sites/:domain/traffic-change-notification/:type/recipients/:recipient",
SiteController,
:remove_traffic_change_notification_recipient

get "/sites/:domain/weekly-report/unsubscribe", UnsubscribeController, :weekly_report
get "/sites/:domain/monthly-report/unsubscribe", UnsubscribeController, :monthly_report

get "/:domain/settings", SiteController, :settings
get "/:domain/settings/general", SiteController, :settings_general
get "/:domain/settings/goals", SiteController, :settings_goals
get "/:domain/settings/properties", SiteController, :settings_props
get "/:domain/settings/email-reports", SiteController, :settings_email_reports

put "/:domain/settings/features/visibility/:setting",
SiteController,
:update_feature_visibility

put "/:domain/settings", SiteController, :update_settings

get "/:domain/export", StatsController, :csv_export
get "/:domain/*path", StatsController, :stats
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -895,6 +895,24 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerSitesCrudApiTest do
assert site.domain_changed_from == nil
end

test "cannot update consolidated view", %{conn: conn, user: user} do
new_site(owner: user)
{:ok, team} = Plausible.Teams.get_or_create(user)
consolidated_view = new_consolidated_view(team)

old_domain = consolidated_view.domain

conn =
put(conn, "/api/v1/sites/#{consolidated_view.domain}", %{
"domain" => "updated.domain.com"
})

assert json_response(conn, 400)["error"] =~
"This operation is unavailable for a consolidated view"

assert Repo.reload!(consolidated_view).domain == old_domain
end

test "fails when team does not match team-scoped key", %{conn: conn, user: user, site: site} do
another_team = new_user() |> subscribe_to_business_plan() |> team_of()
add_member(another_team, user: user, role: :admin)
Expand Down
12 changes: 12 additions & 0 deletions test/plausible_web/controllers/site_controller_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,18 @@ defmodule PlausibleWeb.SiteControllerTest do
describe "GET /:domain/settings/people" do
setup [:create_user, :log_in, :create_site]

on_ee do
test "returns 404 for consolidated view", %{conn: conn, user: user} do
{:ok, team} = Plausible.Teams.get_or_create(user)
new_site(team: team)
new_site(team: team)
consolidated_view = new_consolidated_view(team)

conn = get(conn, "/#{consolidated_view.domain}/settings/people")
assert html_response(conn, 404)
end
end

test "lists current members", %{conn: conn, user: user} do
site = new_site(owner: user)
editor = add_guest(site, role: :editor)
Expand Down
Loading