allow users with admin:metrics to read app metrics
authorFloatingGhost <hannah@coffee-and-dreams.uk>
Fri, 16 Dec 2022 03:32:51 +0000 (03:32 +0000)
committerFloatingGhost <hannah@coffee-and-dreams.uk>
Fri, 16 Dec 2022 03:32:51 +0000 (03:32 +0000)
14 files changed:
.gitignore
lib/pleroma/web/akkoma_api/controllers/metrics_controller.ex
lib/pleroma/web/plugs/rate_limiter.ex
lib/pleroma/web/router.ex
lib/pleroma/web/telemetry.ex
lib/pleroma/web/templates/layout/app.html.eex
lib/pleroma/web/templates/layout/static_fe.html.eex
lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex
lib/pleroma/web/templates/o_auth/o_auth/show.html.eex
mix.exs
priv/static/static-fe/forms.css [new file with mode: 0644]
priv/static/static-fe/static-fe.css
scripts/create_metrics_app.sh [new file with mode: 0755]
test/support/factory.ex

index 14373fb8c557f12924e3bc3b683fa61a9db12547..95b236af62e286baa85790f5d0b8b51d31e6c984 100644 (file)
@@ -76,3 +76,4 @@ docs/site
 
 # docker stuff
 docker-db
+*.iml
index c8d3d8948b7f55af3c6b0dcfe45c298ffced6fc9..8d413bf58717bf77fb2b3c8fa5f7f0b3533774b3 100644 (file)
@@ -3,9 +3,13 @@ defmodule Pleroma.Web.AkkomaAPI.MetricsController do
 
   alias Pleroma.Web.Plugs.OAuthScopesPlug
 
-  @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
-  plug(:skip_auth)
-
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["admin:metrics"]}
+    when action in [
+           :show
+         ]
+  )
 
   def show(conn, _params) do
     stats = TelemetryMetricsPrometheus.Core.scrape()
index 5bebe0ad53fa2ba7d28ddd681eea2a055e0c2955..f5ca27f0c0f1e73f67a2173d639eb3ce885b21a3 100644 (file)
@@ -197,7 +197,11 @@ defmodule Pleroma.Web.Plugs.RateLimiter do
     })
   end
 
-  defp ip(%{remote_ip: remote_ip}) do
+  defp ip(%{remote_ip: remote_ip}) when is_binary(remote_ip) do
+    remote_ip
+  end
+
+  defp ip(%{remote_ip: remote_ip}) when is_tuple(remote_ip) do
     remote_ip
     |> Tuple.to_list()
     |> Enum.join(".")
index c0ce645c44e08bd2cd5f38c57ea70daa400c9fae..f47041b0b8cc1eb1757072e8ce511fb658ea1070 100644 (file)
@@ -868,7 +868,11 @@ defmodule Pleroma.Web.Router do
 
   scope "/" do
     pipe_through([:pleroma_html, :authenticate, :require_admin])
-    live_dashboard("/phoenix/live_dashboard", metrics: {Pleroma.Web.Telemetry, :live_dashboard_metrics}, csp_nonce_assign_key: :csp_nonce)
+
+    live_dashboard("/phoenix/live_dashboard",
+      metrics: {Pleroma.Web.Telemetry, :live_dashboard_metrics},
+      csp_nonce_assign_key: :csp_nonce
+    )
   end
 
   # Test-only routes needed to test action dispatching and plug chain execution
@@ -907,6 +911,7 @@ defmodule Pleroma.Web.Router do
   scope "/", Pleroma.Web.Fallback do
     get("/registration/:token", RedirectController, :registration_page)
     get("/:maybe_nickname_or_id", RedirectController, :redirector_with_meta)
+    get("/api/*path", RedirectController, :api_not_implemented)
     get("/*path", RedirectController, :redirector_with_preload)
 
     options("/*path", RedirectController, :empty)
index 435f557992c233fbdfa0bcb4ff8db3032e705bae..5b01ee14deeb97218b0ea168c78f90153c1a736e 100644 (file)
@@ -11,16 +11,13 @@ defmodule Pleroma.Web.Telemetry do
   def init(_arg) do
     children = [
       {:telemetry_poller, measurements: periodic_measurements(), period: 10_000},
-      {TelemetryMetricsPrometheus, metrics: prometheus_metrics(), plug_cowboy_opts: [ip: {127, 0, 0, 1}]}
+      {TelemetryMetricsPrometheus.Core, metrics: prometheus_metrics()}
     ]
 
     Supervisor.init(children, strategy: :one_for_one)
   end
 
-  @doc """
-  A seperate set of metrics for distributions because phoenix dashboard does NOT handle
-  them well
-  """
+  # A seperate set of metrics for distributions because phoenix dashboard does NOT handle them well
   defp distribution_metrics do
     [
       distribution(
@@ -110,8 +107,6 @@ defmodule Pleroma.Web.Telemetry do
       summary("vm.total_run_queue_lengths.total"),
       summary("vm.total_run_queue_lengths.cpu"),
       summary("vm.total_run_queue_lengths.io"),
-
-
       last_value("pleroma.local_users.total"),
       last_value("pleroma.domains.total"),
       last_value("pleroma.local_statuses.total")
index e33bada858e979173d92ef45e65c55bf3f077340..31e6ec52bd067f6d308d984739481626fc045e31 100644 (file)
@@ -4,17 +4,33 @@
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui">
     <title><%= Pleroma.Config.get([:instance, :name]) %></title>
-    <link rel="stylesheet" href="/instance/static.css">
+    <link rel="stylesheet" href="/static-fe/static-fe.css">
+    <link rel="stylesheet" href="/static-fe/forms.css">
   </head>
   <body>
-    <div class="instance-header">
-      <a class="instance-header__content" href="/">
-        <img class="instance-header__thumbnail" src="<%= Pleroma.Config.get([:instance, :instance_thumbnail]) %>">
-        <h1 class="instance-header__title"><%= Pleroma.Config.get([:instance, :name]) %></h1>
-      </a>
-    </div>
+
+    <div class="background-image"></div>
+    <nav>
+      <div class="inner-nav">
+        <a class="site-brand" href="/">
+          <img class="favicon" src="/favicon.png" />
+          <span><%= Pleroma.Config.get([:instance, :name]) %></span>
+        </a>
+      </div>
+    </nav>
     <div class="container">
-      <%= @inner_content %>
+      <div class="underlay"></div>
+      <div class="column main flex">
+        <div class="panel oauth">
+          <%= @inner_content %>
+        </div>
+      </div>
     </div>
   </body>
+
+  <style>
+  :root {
+    --background-image: url("<%= Pleroma.Config.get([:instance, :background_image]) %>");
+  }
+  </style>
 </html>
index 3d55393f0f846bf8b9f63acb45d4f07dd50ddce4..d159eb901cd1dc25020d56ff37baa6f834e7883e 100644 (file)
@@ -20,8 +20,8 @@
     </nav>
     <div class="container">
       <div class="underlay"></div>
-         <div class="column main">
-        <%= @inner_content %>
+      <div class="column main">
+          <%= @inner_content %>
       </div>
       <div class="column sidebar">
         <div class="about panel">
index 76ed3fda5e7c0f36decbb3219a8bef08285098d4..17e54fb4287ce6744afc59264a87503c318ba7c5 100644 (file)
@@ -1,2 +1,8 @@
-<h1><%= Gettext.dpgettext("static_pages", "oauth authorized page title", "Successfully authorized") %></h1>
-<h2><%= raw Gettext.dpgettext("static_pages", "oauth token code message", "Token code is <br>%{token}", token: safe_to_string(html_escape(@auth.token))) %></h2>
+<div>
+    <div class="panel-heading">
+        <%= Gettext.dpgettext("static_pages", "oauth authorized page title", "Successfully authorized") %>
+    </div>
+    <div class="panel-content">
+        <%= raw Gettext.dpgettext("static_pages", "oauth token code message", "Token code is <br>%{token}", token: safe_to_string(html_escape(@auth.token))) %>
+    </div>
+</div>
index a2f41618e66f0ec0f3f637a10162966ae542ff1d..48c3c5eb92afdf79bf53a25c095f64799760fc7f 100644 (file)
@@ -20,7 +20,9 @@
 
 <div class="container__content">
   <%= if @app do %>
-    <p><%= raw Gettext.dpgettext("static_pages", "oauth authorize message", "Application <strong>%{client_name}</strong> is requesting access to your account.", client_name: safe_to_string(html_escape(@app.client_name))) %></p>
+    <div class="panel-heading">
+      <p><%= raw Gettext.dpgettext("static_pages", "oauth authorize message", "Application <strong>%{client_name}</strong> is requesting access to your account.", client_name: safe_to_string(html_escape(@app.client_name))) %></p>
+    </div>
     <%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f}) %>
   <% end %>
 
diff --git a/mix.exs b/mix.exs
index b5b7efd0c5b98783933b4018e68710d705c55191..4898591b1d79eb4256256f9ae92de4ec1d7d34a6 100644 (file)
--- a/mix.exs
+++ b/mix.exs
@@ -163,7 +163,7 @@ defmodule Pleroma.Mixfile do
       {:telemetry, "~> 0.3"},
       {:telemetry_poller, "~> 0.4"},
       {:telemetry_metrics, "~> 0.4"},
-      {:telemetry_metrics_prometheus, "~> 1.1.0"},
+      {:telemetry_metrics_prometheus_core, "~> 1.1.0"},
       {:poolboy, "~> 1.5"},
       {:recon, "~> 2.5"},
       {:joken, "~> 2.0"},
diff --git a/priv/static/static-fe/forms.css b/priv/static/static-fe/forms.css
new file mode 100644 (file)
index 0000000..f7196ee
--- /dev/null
@@ -0,0 +1,114 @@
+form {
+    width: 100%;
+}
+
+.input {
+    color: var(--muted-text-color);
+    display: flex;
+    margin-left: 1em;
+    margin-right: 1em;
+    flex-direction: column;
+}
+
+input {
+    padding: 10px;
+    margin-top: 5px;
+    margin-bottom: 10px;
+    background-color: var(--background-color);
+    color: var(--primary-text-color);
+    border: 0;
+    transition-property: border-bottom;
+    transition-duration: 0.35s;
+    border-bottom: 2px solid #2a384a;
+    font-size: 14px;
+    width: inherit;
+    box-sizing: border-box;
+}
+
+.scopes-input {
+    display: flex;
+    flex-direction: column;
+    margin: 1em 0;
+    color: var(--muted-text-color);
+}
+
+.scopes-input label:first-child {
+    height: 2em;
+}
+
+.scopes {
+    display: flex;
+    flex-wrap: wrap;
+    color: var(--primary-text-color);
+}
+
+.scope {
+    display: flex;
+    flex-basis: 100%;
+    height: 2em;
+    align-items: center;
+}
+
+.scope:before {
+    color: var(--primary-text-color);
+    content: "✔\fe0e";
+    margin-left: 1em;
+    margin-right: 1em;
+}
+
+[type="checkbox"]+label {
+    display: none;
+    cursor: pointer;
+    margin: 0.5em;
+}
+
+[type="checkbox"] {
+    display: none;
+}
+
+[type="checkbox"]+label:before {
+    cursor: pointer;
+    display: inline-block;
+    color: white;
+    background-color: var(--background-color);
+    border: 4px solid var(--background-color);
+    box-shadow: 0px 0px 1px 0 var(--brand-color);
+    width: 1.2em;
+    height: 1.2em;
+    margin-right: 1.0em;
+    content: "";
+    transition-property: background-color;
+    transition-duration: 0.35s;
+    color: var(--background-color);
+    margin-bottom: -0.2em;
+    border-radius: 2px;
+}
+
+[type="checkbox"]:checked+label:before {
+    background-color: var(--brand-color);
+}
+
+a.button,
+button {
+  width: 100%;
+  background-color: #1c2a3a;
+  color: var(--primary-text-color);
+  border-radius: 4px;
+  border: none;
+  padding: 10px 16px;
+  margin-top: 20px;
+  margin-bottom: 20px;
+  text-transform: uppercase;
+  font-size: 16px;
+  box-shadow: 0px 0px 2px 0px black,
+    0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset,
+    0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
+}
+
+a.button:hover,
+button:hover {
+  cursor: pointer;
+  box-shadow: 0px 0px 0px 1px var(--brand-color),
+    0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset,
+    0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
+}
index 657556077f3a06687670005a6e7e389c29dbc746..606c07d7ed1ef402d82d1ede156ca83f3afcfa9b 100644 (file)
     --profileTint: rgba(15, 22, 30, 0.5);
     --btnText: rgba(185, 185, 186, 1);
     --btn: rgba(21, 30, 43, 1);
-    --btnShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1) , 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
+    --btnShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
     --btnHoverShadow: 0px 0px 1px 2px rgba(185, 185, 186, 0.4) inset, 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
     --lightText: rgba(236, 236, 236, 1);
-    --panelShadow: 0px 0px 3px 0px rgba(0, 0, 0, 0.5) , 0px 4px 6px 3px rgba(0, 0, 0, 0.3);
-    --panelHeaderShadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4) , 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset;
+    --panelShadow: 0px 0px 3px 0px rgba(0, 0, 0, 0.5), 0px 4px 6px 3px rgba(0, 0, 0, 0.3);
+    --panelHeaderShadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset;
     --topBar: rgba(21, 30, 43, 1);
     --topBarText: rgba(159, 159, 161, 1);
-    --topBarShadow: 0px 1px 4px 0px rgba(0, 0, 0, 0.4) , 0px 2px 7px 0px rgba(0, 0, 0, 0.3);
+    --topBarShadow: 0px 1px 4px 0px rgba(0, 0, 0, 0.4), 0px 2px 7px 0px rgba(0, 0, 0, 0.3);
     --underlay: rgba(9, 14, 20, 0.6);
     --background: rgba(15, 22, 30, 1);
     --faint: rgba(185, 185, 186, 0.5);
     --border: rgba(26, 37, 53, 1);
     --poll: rgba(99, 84, 72, 1);
 }
+
 @media (prefers-color-scheme: light) {
     :root {
-        --icon-filter: invert(67%) sepia(7%) saturate(525%) hue-rotate(173deg) brightness(90%) contrast(92%);;
+        --icon-filter: invert(67%) sepia(7%) saturate(525%) hue-rotate(173deg) brightness(90%) contrast(92%);
+        ;
         --wallpaper: rgba(248, 250, 252, 1);
         --alertNeutral: rgba(48, 64, 85, 0.5);
         --alertNeutralText: rgba(0, 0, 0, 1);
         --profileTint: rgba(242, 246, 249, 0.5);
         --btnText: rgba(48, 64, 85, 1);
         --btn: rgba(214, 223, 237, 1);
-        --btnShadow: 0px 0px 2px 0px rgba(0, 0, 0, 0.2) , 0px 1px 0px 0px rgba(255, 255, 255, 0.5) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
-        --btnHoverShadow: 0px 0px 2px 0px rgba(0, 0, 0, 0.2) , 0px 0px 1px 2px rgba(255, 195, 159, 1) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
+        --btnShadow: 0px 0px 2px 0px rgba(0, 0, 0, 0.2), 0px 1px 0px 0px rgba(255, 255, 255, 0.5) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
+        --btnHoverShadow: 0px 0px 2px 0px rgba(0, 0, 0, 0.2), 0px 0px 1px 2px rgba(255, 195, 159, 1) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
         --lightText: rgba(11, 14, 19, 1);
-        --panelShadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5) , 0px 3px 6px 1px rgba(0, 0, 0, 0.2);
+        --panelShadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5), 0px 3px 6px 1px rgba(0, 0, 0, 0.2);
         --panelHeaderShadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.5) inset, 0px 1px 3px 0px rgba(0, 0, 0, 0.3);
         --topBar: rgba(214, 223, 237, 1);
         --topBarText: rgba(48, 64, 85, 1);
@@ -119,7 +121,7 @@ nav {
     padding-right: 5px
 }
 
-body > .container {
+body>.container {
     display: grid;
     grid-template-columns: minmax(25em, 45em) 25em;
     grid-template-areas: "content sidebar";
@@ -155,6 +157,10 @@ body > .container {
     box-shadow: var(--panelHeaderShadow);
 }
 
+.panel-content {
+    padding: 1em;
+}
+
 .about-content {
     padding: 0.6em;
 }
@@ -169,6 +175,18 @@ body > .container {
     padding-left: 0.5em;
 }
 
+.column.flex {
+    grid-column-end: sidebar-end;
+}
+
+.scopes-input {
+    display: flex;
+    flex-direction: column;
+    margin: 1em 0;
+    color: var(--muted-text-color);
+}
+
+
 .status-container,
 .repeat-header,
 .user-card {
@@ -193,6 +211,7 @@ body > .container {
 .repeat-header .right-side {
     color: var(--faint);
 }
+
 .repeat-header .u-photo {
     height: 20px;
     width: 20px;
@@ -255,6 +274,7 @@ body > .container {
 .reply-to-link {
     color: var(--faint);
 }
+
 .reply-to-link:hover {
     text-decoration: underline;
 }
@@ -280,11 +300,13 @@ body > .container {
     margin-bottom: 8px;
 }
 
-header a, .h-card a {
+header a,
+.h-card a {
     text-decoration: none;
 }
 
-header a:hover, .h-card a:hover {
+header a:hover,
+.h-card a:hover {
     text-decoration: underline;
 }
 
@@ -307,7 +329,7 @@ header a:hover, .h-card a:hover {
     min-width: 0;
 }
 
-.attachment > * {
+.attachment>* {
     width: 100%;
     object-fit: contain;
 }
@@ -322,6 +344,7 @@ header a:hover, .h-card a:hover {
     display: flex;
     align-items: center;
 }
+
 .nsfw-banner div {
     width: 100%;
     text-align: center;
@@ -330,6 +353,7 @@ header a:hover, .h-card a:hover {
 .nsfw-banner:not(:hover) {
     background-color: var(--background);
 }
+
 .nsfw-banner:hover div {
     display: none;
 }
@@ -342,10 +366,12 @@ header a:hover, .h-card a:hover {
     word-break: break-word;
     z-index: 1;
 }
+
 .poll-option .percentage {
     width: 3.5em;
     flex-shrink: 0;
 }
+
 .poll-option .fill {
     height: 100%;
     position: absolute;
@@ -362,7 +388,8 @@ header a:hover, .h-card a:hover {
     display: flex;
     margin-top: 0.75em;
 }
-.status-actions > * {
+
+.status-actions>* {
     max-width: 4em;
     flex: 1;
     display: flex;
@@ -458,11 +485,11 @@ summary {
     right: 0;
     bottom: 0;
     background-image: linear-gradient(to bottom, var(--profileTint), var(--profileTint)),
-                      var(--user-banner);
+        var(--user-banner);
     background-size: cover;
     background-color: var(--profileBg);
     -webkit-mask: linear-gradient(to top, white, transparent) bottom no-repeat,
-                  linear-gradient(to top, white, white);
+        linear-gradient(to top, white, white);
     -webkit-mask-composite: xor;
     -webkit-mask-size: 100% 60%;
     z-index: -2;
@@ -600,7 +627,7 @@ summary {
 }
 
 @media (max-width: 800px) {
-    body > .container {
+    body>.container {
         display: block;
     }
 
@@ -624,4 +651,4 @@ img:not(.u-photo, .fa-icon) {
 .username img:not(.u-photo) {
     width: 16px;
     height: 16px;
-}
+}
\ No newline at end of file
diff --git a/scripts/create_metrics_app.sh b/scripts/create_metrics_app.sh
new file mode 100755 (executable)
index 0000000..c2f32e5
--- /dev/null
@@ -0,0 +1,65 @@
+#!/bin/sh
+
+read -p "Instance URL (e.g https://example.com): " INSTANCE_URL
+
+echo "Creating oauth app..."
+
+RESP=$(curl \
+    -XPOST \
+    $INSTANCE_URL/api/v1/apps \
+    --silent \
+    --data-urlencode 'client_name=fedibash' \
+    --data-urlencode 'redirect_uris=urn:ietf:wg:oauth:2.0:oob' \
+    --data-urlencode 'scopes=admin:metrics' \
+    --header "Content-Type: application/x-www-form-urlencoded"
+)
+
+client_id=$(echo $RESP | jq -r .client_id)
+client_secret=$(echo $RESP | jq -r .client_secret)
+
+if [ -z "$client_id"]; then
+  echo "Could not create an app"
+  echo "$RESP"
+  exit 1
+fi
+
+echo "Please visit the following URL and input the code provided"
+AUTH_URL="$INSTANCE_URL/oauth/authorize?client_id=$client_id&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=admin:metrics&response_type=code"
+if [ ! -z "$BROWSER" ]; then
+  $BROWSER $AUTH_URL
+fi;
+
+echo $AUTH_URL
+
+read -p "Code: " CODE
+
+echo "Requesting code..."
+
+RESP=$(curl \
+    -XPOST \
+    $INSTANCE_URL/oauth/token \
+    --silent \
+    --header "Content-Type: application/x-www-form-urlencoded" \
+    --data-urlencode "client_id=$client_id" \
+    --data-urlencode "client_secret=$client_secret" \
+    --data-urlencode "code=$CODE" \
+    --data-urlencode "grant_type=authorization_code" \
+    --data-urlencode 'redirect_uri=urn:ietf:wg:oauth:2.0:oob' \
+    --data-urlencode "scope=admin:metrics"
+)
+
+ACCESS_TOKEN="$(echo $RESP | jq -r .access_token)"
+
+echo "Token is $ACCESS_TOKEN"
+DOMAIN=$(echo $INSTANCE_URL | sed -e 's/^https:\/\///')
+
+echo "Use the following config in your prometheus.yml:
+- job_name: akkoma
+  scheme: https
+  authorization:
+    credentials: $ACCESS_TOKEN
+  metrics_path: /api/v1/akkoma/metrics
+  static_configs:
+  - targets:
+    - $DOMAIN
+"
index 808f8f8879e9148bca2dd0e63e6dd5f0c8fed596..84e076137401a17e834eb21c39dba7e94d343c02 100644 (file)
@@ -554,7 +554,7 @@ defmodule Pleroma.Factory do
     %Pleroma.Web.OAuth.App{
       client_name: sequence(:client_name, &"Some client #{&1}"),
       redirect_uris: "https://example.com/callback",
-      scopes: ["read", "write", "follow", "push", "admin"],
+      scopes: ["read", "write", "follow", "push"],
       website: "https://example.com",
       client_id: Ecto.UUID.generate(),
       client_secret: "aaa;/&bbb"