Skip to content


Subversion checkout URL

You can clone with
Download ZIP

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
  • 2 commits
  • 7 files changed
  • 0 commit comments
  • 1 contributor
14 README.rst
@@ -21,17 +21,13 @@ django-sql-explorer is MIT licensed, and pull requests are welcome!
.. image::
-**Query timings!**
-**"Template" columns for quick-linking to detailed record views**
-- **Safety & Security**
+- **Security**
- Let's not kid ourselves - this tool is all about giving people access to running SQL in production. So if that makes you nervous (and it should) - you've been warned. Explorer makes an effort to not allow terrible things to happen, but be careful! Note there is a setting in the tip (master) to use a different SQL connection than the default django connection. It's recommended you use a read-only database role.
- - Admins-only per default. Nice try randos! You have to pass the is_staff() test to access explorer. This can be configuered with the settings EXPLORER_PERMISSION_VIEW and EXPLORER_PERMISSION_CHANGE
+ - Explorer supports two different permission checks for users of the tool. Users passing the EXPLORER_PERMISSION_CHANGE test can create, edit, delete, and execute queries. Users who do not pass this test but pass the EXPLORER_PERMISSION_VIEW test can only execute queries. Other users cannot access any part of Explorer. Both permission groups are set to is_staff by default and can be overridden in your settings file.
- Enforces a SQL blacklist so destructive queries don't get executed (delete, drop, alter, update etc). This is not bulletproof and it's recommended that you instead configure a read-only database role, but when not possible the blacklist provides reasonable protection.
- **Easy to get started**
- 100% built on Django's ORM, so works with Postgresql, Mysql, and Sqlite.
@@ -50,7 +46,7 @@ Features
- Let's say you have a query like 'select id, email from user' and you'd like to quickly drill through to the profile page for each user in the result. You can create a "template" column to do just that.
- Just set up a template column in your settings file:
- ```EXPLORER_TRANSFORMS = [('user', '<a href="{0}/">{0}</a>')]```
+ ``EXPLORER_TRANSFORMS = [('user', '<a href="{0}/">{0}</a>')]``
- And change your query to 'SELECT id AS "user", email FROM user'. Explorer will match the "user" column alias to the transform and merge each cell in that column into the template string. Cool!
@@ -127,7 +123,7 @@ EXPLORER_SQL_WHITELIST These phrases are allowed, even though part of the
EXPLORER_DEFAULT_ROWS The number of rows to show by default in the preview pane. 100
EXPLORER_SCHEMA_EXCLUDE_APPS Don't show schema for these packages in the schema helper. ('django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.admin')
EXPLORER_CONNECTION_NAME The name of the Django database connection to use. Ideally set this to a connection with read only permissions None # Which means use the 'default' connection
-EXPLORER_PERMISSION_VIEW Callback to check if the user is allowed to view and execute stored queries Checks for the user to be staff
-EXPLORER_PERMISSION_CHANGE Callback to check if the user is allowed to add/change/delete queries Checks for the user to be staff
+EXPLORER_PERMISSION_VIEW Callback to check if the user is allowed to view and execute stored queries lambda u: u.is_staff
+EXPLORER_PERMISSION_CHANGE Callback to check if the user is allowed to add/change/delete queries lambda u: u.is_staff
EXPLORER_TRANSFORMS List of tuples like [('alias', 'Template for {0}')]. See features section of this doc for more info. []
============================ =============================================================================================================== ================================================================================================================================================
9 explorer/
@@ -12,11 +12,6 @@
-def sql_explorer_view(user):
- return user.is_staff
-EXPLORER_PERMISSION_VIEW = getattr(settings, 'EXPLORER_PERMISSION_VIEW', sql_explorer_view)
+EXPLORER_PERMISSION_VIEW = getattr(settings, 'EXPLORER_PERMISSION_VIEW', lambda u: u.is_staff)
-def sql_explorer_change(user):
- return user.is_staff
-EXPLORER_PERMISSION_CHANGE = getattr(settings, 'EXPLORER_PERMISSION_CHANGE', sql_explorer_change)
+EXPLORER_PERMISSION_CHANGE = getattr(settings, 'EXPLORER_PERMISSION_CHANGE', lambda u: u.is_staff)
9 explorer/
@@ -1,9 +1,6 @@
-from django.contrib.auth import get_user_model
-from django.contrib.auth.decorators import user_passes_test
from explorer.utils import passes_blacklist, write_csv, swap_params, execute_query, execute_and_fetch_query, extract_params, shared_dict_update
from django.db import models, DatabaseError
from django.core.urlresolvers import reverse
MSG_FAILED_BLACKLIST = "Query failed the SQL blacklist."
@@ -60,8 +57,4 @@ def available_params(self):
return p
def get_absolute_url(self):
- return reverse("query_detail", kwargs={'query_id':})
-User = get_user_model()
-User.add_to_class('sql_explorer_view', EXPLORER_PERMISSION_VIEW)
-User.add_to_class('sql_explorer_change', EXPLORER_PERMISSION_CHANGE)
+ return reverse("query_detail", kwargs={'query_id':})
2  explorer/templates/explorer/play.html
@@ -1,7 +1,7 @@
{% extends "explorer/base.html" %}
{% block navlinks %}
- {% if request.user.sql_explorer_change %}
+ {% if can_change %}
<li><a href="../new/">New Query</a></li>
<li class="active"><a href="#">Playground</a></li>
{% endif %}
4 explorer/templates/explorer/query.html
@@ -2,7 +2,7 @@
{% load url from future %}
{% block navlinks %}
- {% if request.user.sql_explorer_change %}
+ {% if can_change %}
<li{% if not query %} class="active" {% endif %}><a href="../new/">New Query</a></li>
<li><a href="../play/">Playground</a></li>
{% endif %}
@@ -17,7 +17,7 @@ <h2 style="padding-bottom: 20px">{% if query %}{{ query.title }}{% else %}New Qu
<div class="alert alert-info">{{ message }}</div>
{% endif %}
- {% if request.user.sql_explorer_change %}
+ {% if can_change %}
<form role="form" class="form-horizontal" action="../{% firstof 'new' %}/" method="post" id="editor">{% csrf_token %}
{% if error %}
<div class="alert alert-danger">{{ error|escape }}</div>
6 explorer/templates/explorer/query_list.html
@@ -1,7 +1,7 @@
{% extends "explorer/base.html" %}
{% block navlinks %}
- {% if request.user.sql_explorer_change %}
+ {% if can_change %}
<li><a href="new/">New Query</a></li>
<li><a href="play/">Playground</a></li>
{% endif %}
@@ -14,7 +14,7 @@
<th>Download CSV</th>
- {% if request.user.sql_explorer_change %}
+ {% if can_change %}
{% endif %}
@@ -26,7 +26,7 @@
<td style="padding: 2px 0px 0px 45px; vertical-align: middle">
<a href="{{ }}/download"><i class="glyphicon glyphicon-download" style="font-size: 20px;"></i></a>
- {% if request.user.sql_explorer_change %}
+ {% if can_change %}
<td style="padding: 0px 0px 0px 20px; vertical-align: middle">
<a href="{{ }}/delete"><i class="glyphicon glyphicon-trash" style="font-size: 18px;"></i></a>
6 explorer/
@@ -135,7 +135,7 @@ def get(self, request, query_id):
return render_to_response('explorer/query.html', vm)
def post(self, request, query_id):
- if not request.user.sql_explorer_change():
+ if not EXPLORER_PERMISSION_CHANGE(request.user):
return HttpResponseRedirect(
reverse_lazy('query_detail', kwargs={'query_id': query_id})
@@ -167,5 +167,7 @@ def query_viewmodel(request, query, title=None, form=None, message=None):
'headers': headers,
'duration': duration,
'rows': rows,
- 'total_rows': len(data)}
+ 'total_rows': len(data),
+ 'can_view': EXPLORER_PERMISSION_VIEW(request.user),
+ 'can_change': EXPLORER_PERMISSION_CHANGE(request.user)}

No commit comments for this range

Something went wrong with that request. Please try again.