Skip to content

Commit

Permalink
Shard: Add support for HTTP Basic Auth
Browse files Browse the repository at this point in the history
The API end point of some shards are protected with HTTP Basic Auth. This will
make the promql-query component to fail receiving a '401 Unauthorized' error
response.

To address that, we have added the possibility to specify HTTP Basic Auth
credentials on shards.

Apart from that, the query is performed in the back end for security issues.
Since the request's "Authorization" header contains the base64-encoded user and
password, if we performed the query in the front end that information would be
easy to obtain by inspecting the HTTP requests.
  • Loading branch information
vincent-olivert-riera committed Jun 4, 2024
1 parent 4ed6d1b commit 4eee35c
Show file tree
Hide file tree
Showing 8 changed files with 96 additions and 8 deletions.
36 changes: 36 additions & 0 deletions promgen/migrations/0023_shard_basic_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Generated by Django 4.2.11 on 2024-06-04 07:52

from django.db import migrations, models

import promgen.validators


class Migration(migrations.Migration):
dependencies = [
("promgen", "0022_rule_labels_annotations"),
]

operations = [
migrations.AddField(
model_name="shard",
name="basic_auth",
field=models.BooleanField(
default=False, help_text="This shard's API requires HTTP Basic Auth"
),
),
migrations.AddField(
model_name="shard",
name="basic_auth_password",
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name="shard",
name="basic_auth_user",
field=models.CharField(
blank=True,
max_length=255,
null=True,
validators=[promgen.validators.http_basic_auth_user_id],
),
),
]
8 changes: 8 additions & 0 deletions promgen/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,14 @@ class Shard(models.Model):
default=10000,
help_text="Estimated Target Count",
)
basic_auth = models.BooleanField(
default=False,
help_text="This shard's API requires HTTP Basic Auth",
)
basic_auth_user = models.CharField(
max_length=255, validators=[validators.http_basic_auth_user_id], blank=True, null=True
)
basic_auth_password = models.CharField(max_length=255, blank=True, null=True)

class Meta:
ordering = ["name"]
Expand Down
10 changes: 6 additions & 4 deletions promgen/static/js/promgen.vue.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ app.component('silence-form', {

app.component("promql-query", {
delimiters: ['[[', ']]'],
props: ["href", "query", "max"],
props: ["shard", "query", "max"],
data: function () {
return {
count: 0,
Expand All @@ -207,9 +207,11 @@ app.component("promql-query", {
},
template: '#promql-query-template',
mounted() {
var url = new URL(this.href);
url.search = new URLSearchParams({ query: this.query });
fetch(url)
const params = new URLSearchParams({
shard: this.shard,
query: this.query,
});
fetch(`/promql-query?${params}`)
.then(response => response.json())
.then(result => this.count = Number.parseInt(result.data.result[0].value[1]))
.finally(() => this.ready = true);
Expand Down
4 changes: 2 additions & 2 deletions promgen/templates/promgen/project_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,10 @@ <h1>Register new Project</h1>
</td>
{% if shard.proxy %}
<td>
<promql-query class="label" href='{{shard.url}}/api/v1/query' query='sum(scrape_samples_scraped)' max="{{shard.samples}}">Samples: </promql-query>
<promql-query class="label" shard='{{shard.id}}' query='sum(scrape_samples_scraped)' max="{{shard.samples}}">Samples: </promql-query>
</td>
<td>
<promql-query class="label" href='{{shard.url}}/api/v1/query' query='count(up)' max="{{shard.targets}}">Exporters: </promql-query>
<promql-query class="label" shard='{{shard.id}}' query='count(up)' max="{{shard.targets}}">Exporters: </promql-query>
</td>
{% else %}
<td>&nbsp;</td>
Expand Down
4 changes: 2 additions & 2 deletions promgen/templates/promgen/shard_header.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{% load i18n %}
<div class="well">
{% if shard.proxy %}
<promql-query class="label" href='{{shard.url}}/api/v1/query' query='sum(scrape_samples_scraped)' max="{{shard.samples}}">Samples: </promql-query>
<promql-query class="label" href='{{shard.url}}/api/v1/query' query='count(up)' max="{{shard.targets}}">Exporters: </promql-query>
<promql-query class="label" shard='{{shard.id}}' query='sum(scrape_samples_scraped)' max="{{shard.samples}}">Samples: </promql-query>
<promql-query class="label" shard='{{shard.id}}' query='count(up)' max="{{shard.targets}}">Exporters: </promql-query>
{% endif %}

{% if user.is_superuser %}
Expand Down
2 changes: 2 additions & 0 deletions promgen/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@
path("proxy/v1/silences/<silence_id>", csrf_exempt(proxy.ProxyDeleteSilence.as_view()), name="proxy-silence-delete"),
# Promgen rest API
path("rest/", include((router.urls, "api"), namespace="api")),
# PromQL Query
path("promql-query", views.PromqlQuery.as_view(), name="promql-query"),
]

try:
Expand Down
10 changes: 10 additions & 0 deletions promgen/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,13 @@ def datetime(value):
parser.parse(value)
except ValueError:
raise ValidationError("Invalid timestamp")


def http_basic_auth_user_id(value):
"""
Validate a string as an HTTP Basic Auth user-id.
A valid user-id must not contain colons (:).
"""
if ":" in value:
raise ValidationError("Invalid user-id")
30 changes: 30 additions & 0 deletions promgen/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import logging
import platform
import time
from base64 import b64encode
from itertools import chain

import prometheus_client
Expand All @@ -28,6 +29,7 @@
from django.views.generic.detail import SingleObjectMixin
from django.views.generic.edit import CreateView, DeleteView, FormView
from prometheus_client.core import CounterMetricFamily, GaugeMetricFamily
from requests.exceptions import HTTPError

import promgen.templatetags.promgen as macro
from promgen import (
Expand Down Expand Up @@ -1374,3 +1376,31 @@ def post(self, request, pk):
return JsonResponse(
{request.POST["target"]: render_to_string("promgen/ajax_clause_check.html", result)}
)


class PromqlQuery(View):
def get(self, request):
if not all(x in request.GET for x in ["shard", "query"]):
return HttpResponse("BAD REQUEST", status=400)

shard = models.Shard.objects.get(pk=request.GET["shard"])
params = {"query": request.GET["query"]}
headers = {}

if shard.basic_auth:
user_pass = b64encode(
f"{shard.basic_auth_user}:{shard.basic_auth_password}".encode()
).decode()
headers["Authorization"] = f"Basic {user_pass}"

try:
response = util.get(f"{shard.url}/api/v1/query", params=params, headers=headers)
response.raise_for_status()
except HTTPError:
return HttpResponse(
response.content,
content_type=response.headers["content-type"],
status=response.status_code,
)

return HttpResponse(response.content, content_type="application/json")

0 comments on commit 4eee35c

Please sign in to comment.