Empowering Operations: Grafana Proxy Integration with Django

pallav kumar
Capillary Technologies
4 min readMay 8, 2024

Introduction:

In the dynamic landscape of operational tasks, the need for centralized tools is paramount. One such requirement we encountered involved exposing Grafana dashboards to both internal (core tech team) and external (non-core tech team) users. However, the methods for exposing these dashboards to internal and external users differed significantly. Internal users faced no restrictions, while external users were subject to several limitations.

Problem Statement:

Our Django application serves as the Operations Hub for our internal and external teams for different use cases, hence we decided to solve it via this Django application. Managing access to Grafana dashboards through Django presents two primary hurdles:

  • Given that our Grafana instance allows anonymous access by default, requiring Ingress Basic Authentication for all teams. This created the necessity for seamless access for both internal and external users, eliminating the need for explicit authentication prompts.
  • Additionally, it’s crucial to limit external user access to only permitted dashboards, preventing direct access to other dashboards and data sources configured within Grafana.

Solution Overview:

To tackle these obstacles, we’ve developed a tailored approach that merges reverse proxying with validations performed both on the client and server sides. Here’s the breakdown of its functionality:

Seamless Dashboard Access for Internal/External Users:

We have used a Django library called revproxy to set up a reverse proxy that forwards requests to the Grafana server. By configuring the reverse proxy, users can access Grafana dashboards seamlessly without needing to provide explicit authentication credentials. This approach eliminates the need for users to remember additional authentication details, improving user experience and accessibility.

At a high level, this is what happens behind the scenes in a request proxied by django-revproxy:

  • Django receives a request from the client and processes it using a view that extends revproxy.proxy.ProxyView.
  • Revproxy will clone the client request.
  • The cloned request is sent to the upstream server with Basic Authorization (set in the view).
  • After receiving the response from upstream, the view will process it to make sure all headers are set properly. Some headers like Location are treated as special cases.
  • We added an extra response header.
proxyRes['X-Frame-Options'] = 'SAMEORIGIN' # This allows Iframe embedding
  • The response received from the upstream server is transformed into a django.http.HttpResponse. For binary files StreamingHttpResponse is used instead to reduce memory usage.
  • Finally, the response will then be returned to the user.

Secure External Viewing for Authorized Users:

External users are granted access to view selected Grafana dashboards but are restricted from accessing other internal dashboards & data sources configured in Grafana directly.

  • Client-side validation ensures that external users can only view dashboards authorized to them via Django UI dropdown and cannot access other dashboards, data sources or perform unauthorized actions. Via overlay we can restrict users to click on specific sections of the Grafana embedded inside an iframe by adjusting height and width. We can use multiple overlay div to disable iframe sections horizontally/vertically.
<div id="overlay" style="position: absolute; top: 0; left: 0; width: 7%; height: 100%; background-color: rgba(255, 255, 255 0);"></div>
  • Server-side validation implemented within Django proxy view ensures that requests from external users are intercepted before being forwarded to Grafana. This approach provides external users to access dashboards in a controlled way where we intercept unauthorized upstream endpoints and rejects them at proxy view layer in Django without compromising data & data source security.

Code Snippets:

  • Django project(urls.py)
from django.urls import re_path, include
from django.contrib import admin
from django.views.generic import RedirectView
from apitest_app.views import proxy
from django.views.decorators.csrf import csrf_exempt

urlpatterns =[
re_path(r'^django_app/tunnel/(?P<path>.*)', RedirectView.as_view(url='/tunnel/%(path)s', permanent=True)),
re_path(r'^(?P<path>tunnel/.*)', csrf_exempt(proxy.MyProxyView.as_view()), name="grafanaproxy"),
  • Proxy view
from django.http import HttpResponseBadRequest, HttpResponseRedirect
from django.urls import reverse
from http import HTTPStatus
from revproxy.views import ProxyView
from common.logger import Logger
from constants.Constants import Constants

class MyProxyView(ProxyView):
def dispatch(self, request, *args, **kwargs):
referer_rejected_endpoints = ['grafana/explore','/grafana/dashboards','/grafana/alerting/list','editview=settings','&search=open','editPanel=']
path_rejected_endponts = ['grafana/api/search','/grafana/api/live/publish','explore.d756ff5fc6a5ac3b66e9.js']

referer_url = request.META.get('HTTP_REFERER', None)
# Check if referer URL exists and ends with any rejected endpoint
if referer_url and any(endpoint in referer_url for endpoint in referer_rejected_endpoints):
response_data = {'error': 'Access to this endpoint is not allowed.'}
return HttpResponseRedirect(reverse('forbidden_page'))

path_url = request.META.get('PATH_INFO',None)
if path_url and any(endpoint in path_url for endpoint in path_rejected_endponts):
response_data = {'error': 'Access to this endpoint is not allowed.'}
return HttpResponseRedirect(reverse('forbidden_page'))

#Based on clusterId in this we have to set upstream & auth
if '123' in request.META['PATH_INFO']:
self.upstream,auth = self.getGrafanadetails('k8sCluster1')
else:
self.upstream,auth = self.getGrafanadetails('k8sCluster2')

original_headers = dict(request.META)
original_headers['HTTP_AUTHORIZATION'] = auth
request.META.update(original_headers)

if request.META.get('HTTP_CONNECTION', '').lower() == 'upgrade' and \
request.META.get('HTTP_UPGRADE', '').lower() == 'websocket':
# Handle WebSocket upgrade
#return self.handle_websocket(request, *args, **kwargs)
return HttpResponseBadRequest("WebSocket requests are not allowed.")
proxyRes = super().dispatch(request, *args, **kwargs)
proxyRes['X-Frame-Options'] = 'SAMEORIGIN' # This allows Iframe embedding
return proxyRes

def handle_websocket(self, request, *args, **kwargs):
# Implement WebSocket handling logic here
Logger.log("WebSocket connection requested.")
# You can either handle WebSocket connections yourself or proxy them to another WebSocket server
return HttpResponse("WebSocket Upgrade Successful", status=HTTPStatus.SWITCHING_PROTOCOLS)

def getGrafanadetails(self,cluster):
for grafanaUrl, auth in Constants.grafanaUrls[cluster].items():
return grafanaUrl,auth

Requests from html iframe will be redirected to proxy view via Django project rules in urls.py and response from proxy server will be returned to HTML iframe. All internal requests originating from Grafana will also be served via proxy view.

Conclusion:

By combining reverse proxying with client-side and server-side validations, we’ve developed a custom solution that addresses the challenges of managing access to Grafana dashboards. Internal users can seamlessly access dashboards, while external users are granted secure viewing privileges without direct access to data sources. This approach enhances user experience, improves security, and ensures compliance with access control policies.

In summary, our custom solution provides a robust framework for managing access to Grafana dashboards in diverse environments, catering to the needs of both internal and external users.

--

--