[#1668] Restricted access to app metrics endpoint by default. Added ability to config...
authorIvan Tashkinov <ivantashkinov@gmail.com>
Sun, 18 Oct 2020 18:22:21 +0000 (21:22 +0300)
committerIvan Tashkinov <ivantashkinov@gmail.com>
Sun, 18 Oct 2020 18:22:21 +0000 (21:22 +0300)
Added tests and documentation.

CHANGELOG.md
config/config.exs
docs/API/prometheus.md
lib/pleroma/helpers/inet_helper.ex [new file with mode: 0644]
lib/pleroma/web/endpoint.ex
test/pleroma/web/endpoint/metrics_exporter_test.exs [new file with mode: 0644]

index 05e94581acd7674ead6faab3e1a84b455d213c10..9f6a31f23245abda439dd9f7c050f31ba514773d 100644 (file)
@@ -12,12 +12,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Media preview proxy (requires `ffmpeg` and `ImageMagick` to be installed and media proxy to be enabled; see `:media_preview_proxy` config for more details).
 - Pleroma API: Importing the mutes users from CSV files.
 - Experimental websocket-based federation between Pleroma instances.
+- App metrics: ability to restrict access to specified IP whitelist.
 
 ### Changed
 
 - **Breaking** Requires `libmagic` (or `file`) to guess file types.
 - **Breaking:** Pleroma Admin API: emoji packs and files routes changed.
 - **Breaking:** Sensitive/NSFW statuses no longer disable link previews.
+- **Breaking:** App metrics endpoint (`/api/pleroma/app_metrics`) is disabled by default, check `docs/API/prometheus.md` on enabling and configuring. 
 - Search: Users are now findable by their urls.
 - Renamed `:await_up_timeout` in `:connections_pool` namespace to `:connect_timeout`, old name is deprecated.
 - Renamed `:timeout` in `pools` namespace to `:recv_timeout`, old name is deprecated.
index 2c614236033863a8560fbeae068f54eb0c24fe51..a7aae58021972da526eacebe781013c48a80e82a 100644 (file)
@@ -636,7 +636,12 @@ config :pleroma, Pleroma.Emails.UserEmail,
 
 config :pleroma, Pleroma.Emails.NewUsersDigestEmail, enabled: false
 
-config :prometheus, Pleroma.Web.Endpoint.MetricsExporter, path: "/api/pleroma/app_metrics"
+config :prometheus, Pleroma.Web.Endpoint.MetricsExporter,
+  enabled: false,
+  auth: false,
+  ip_whitelist: [],
+  path: "/api/pleroma/app_metrics",
+  format: :text
 
 config :pleroma, Pleroma.ScheduledActivity,
   daily_user_limit: 25,
index 19c564e3c57150b7c0081afec68ba76a05cfe5ee..a5158d9052acfa9ba4b089905b5192a3c763299d 100644 (file)
@@ -2,15 +2,37 @@
 
 Pleroma includes support for exporting metrics via the [prometheus_ex](https://github.com/deadtrickster/prometheus.ex) library.
 
+Config example:
+
+```
+config :prometheus, Pleroma.Web.Endpoint.MetricsExporter,
+  enabled: true,
+  auth: {:basic, "myusername", "mypassword"},
+  ip_whitelist: ["127.0.0.1"],
+  path: "/api/pleroma/app_metrics",
+  format: :text
+```
+
+* `enabled` (Pleroma extension) enables the endpoint
+* `ip_whitelist` (Pleroma extension) could be used to restrict access only to specified IPs
+* `auth` sets the authentication (`false` for no auth; configurable to HTTP Basic Auth, see [prometheus-plugs](https://github.com/deadtrickster/prometheus-plugs#exporting) documentation)
+* `format` sets the output format (`:text` or `:protobuf`)
+* `path` sets the path to app metrics page 
+
+
 ## `/api/pleroma/app_metrics`
+
 ### Exports Prometheus application metrics
+
 * Method: `GET`
-* Authentication: not required
+* Authentication: not required by default (see configuration options above)
 * Params: none
-* Response: JSON
+* Response: text
 
 ## Grafana
+
 ### Config example
+
 The following is a config example to use with [Grafana](https://grafana.com)
 
 ```
diff --git a/lib/pleroma/helpers/inet_helper.ex b/lib/pleroma/helpers/inet_helper.ex
new file mode 100644 (file)
index 0000000..126f823
--- /dev/null
@@ -0,0 +1,19 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Helpers.InetHelper do
+  def parse_address(ip) when is_tuple(ip) do
+    {:ok, ip}
+  end
+
+  def parse_address(ip) when is_binary(ip) do
+    ip
+    |> String.to_charlist()
+    |> parse_address()
+  end
+
+  def parse_address(ip) do
+    :inet.parse_address(ip)
+  end
+end
index 56562c12fc5ed46a6c3efcd00ac955a2e51e2afd..1a8fdd8b9d1a7b740e3a428988d662c625aa9e9f 100644 (file)
@@ -7,6 +7,8 @@ defmodule Pleroma.Web.Endpoint do
 
   require Pleroma.Constants
 
+  alias Pleroma.Config
+
   socket("/socket", Pleroma.Web.UserSocket)
 
   plug(Pleroma.Web.Plugs.SetLocalePlug)
@@ -86,19 +88,19 @@ defmodule Pleroma.Web.Endpoint do
   plug(Plug.Parsers,
     parsers: [
       :urlencoded,
-      {:multipart, length: {Pleroma.Config, :get, [[:instance, :upload_limit]]}},
+      {:multipart, length: {Config, :get, [[:instance, :upload_limit]]}},
       :json
     ],
     pass: ["*/*"],
     json_decoder: Jason,
-    length: Pleroma.Config.get([:instance, :upload_limit]),
+    length: Config.get([:instance, :upload_limit]),
     body_reader: {Pleroma.Web.Plugs.DigestPlug, :read_body, []}
   )
 
   plug(Plug.MethodOverride)
   plug(Plug.Head)
 
-  secure_cookies = Pleroma.Config.get([__MODULE__, :secure_cookie_flag])
+  secure_cookies = Config.get([__MODULE__, :secure_cookie_flag])
 
   cookie_name =
     if secure_cookies,
@@ -106,7 +108,7 @@ defmodule Pleroma.Web.Endpoint do
       else: "pleroma_key"
 
   extra =
-    Pleroma.Config.get([__MODULE__, :extra_cookie_attrs])
+    Config.get([__MODULE__, :extra_cookie_attrs])
     |> Enum.join(";")
 
   # The session will be stored in the cookie and signed,
@@ -116,7 +118,7 @@ defmodule Pleroma.Web.Endpoint do
     Plug.Session,
     store: :cookie,
     key: cookie_name,
-    signing_salt: Pleroma.Config.get([__MODULE__, :signing_salt], "CqaoopA2"),
+    signing_salt: Config.get([__MODULE__, :signing_salt], "CqaoopA2"),
     http_only: true,
     secure: secure_cookies,
     extra: extra
@@ -136,8 +138,34 @@ defmodule Pleroma.Web.Endpoint do
     use Prometheus.PlugExporter
   end
 
+  defmodule MetricsExporterCaller do
+    @behaviour Plug
+
+    def init(opts), do: opts
+
+    def call(conn, opts) do
+      prometheus_config = Application.get_env(:prometheus, MetricsExporter, [])
+      ip_whitelist = List.wrap(prometheus_config[:ip_whitelist])
+
+      cond do
+        !prometheus_config[:enabled] ->
+          conn
+
+        ip_whitelist != [] and
+            !Enum.find(ip_whitelist, fn ip ->
+              Pleroma.Helpers.InetHelper.parse_address(ip) == {:ok, conn.remote_ip}
+            end) ->
+          conn
+
+        true ->
+          MetricsExporter.call(conn, opts)
+      end
+    end
+  end
+
   plug(PipelineInstrumenter)
-  plug(MetricsExporter)
+
+  plug(MetricsExporterCaller)
 
   plug(Pleroma.Web.Router)
 
diff --git a/test/pleroma/web/endpoint/metrics_exporter_test.exs b/test/pleroma/web/endpoint/metrics_exporter_test.exs
new file mode 100644 (file)
index 0000000..f954cc1
--- /dev/null
@@ -0,0 +1,69 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Endpoint.MetricsExporterTest do
+  use Pleroma.Web.ConnCase
+
+  alias Pleroma.Web.Endpoint.MetricsExporter
+
+  defp config do
+    Application.get_env(:prometheus, MetricsExporter)
+  end
+
+  describe "with default config" do
+    test "does NOT expose app metrics", %{conn: conn} do
+      conn
+      |> get(config()[:path])
+      |> json_response(404)
+    end
+  end
+
+  describe "when enabled" do
+    setup do
+      initial_config = config()
+      on_exit(fn -> Application.put_env(:prometheus, MetricsExporter, initial_config) end)
+
+      Application.put_env(
+        :prometheus,
+        MetricsExporter,
+        Keyword.put(initial_config, :enabled, true)
+      )
+    end
+
+    test "serves app metrics", %{conn: conn} do
+      conn = get(conn, config()[:path])
+      assert response = response(conn, 200)
+
+      for metric <- [
+            "http_requests_total",
+            "http_request_duration_microseconds",
+            "phoenix_controller_render_duration",
+            "phoenix_controller_call_duration",
+            "telemetry_scrape_duration",
+            "erlang_vm_memory_atom_bytes_total"
+          ] do
+        assert response =~ ~r/#{metric}/
+      end
+    end
+
+    test "when IP whitelist configured, " <>
+           "serves app metrics only if client IP is whitelisted",
+         %{conn: conn} do
+      Application.put_env(
+        :prometheus,
+        MetricsExporter,
+        Keyword.put(config(), :ip_whitelist, ["127.127.127.127", {1, 1, 1, 1}, '255.255.255.255'])
+      )
+
+      conn
+      |> get(config()[:path])
+      |> json_response(404)
+
+      conn
+      |> Map.put(:remote_ip, {127, 127, 127, 127})
+      |> get(config()[:path])
+      |> response(200)
+    end
+  end
+end