Adding security features to Django projects
Security is most valuable feature of any software, and each developer should keep in mind security issues during programming. In this article I show how to restrict user's access to view, but not modify objects in Django project. It could be an equivalent of 'Readers' field in Lotus Notes/Domino application.
First of all, let's set up Django.
Check out Django’s main development branch (the ‘trunk’) like so:
svn co http://code.djangoproject.com/svn/django/trunk/ django-trunk
Install it:
cd django-trunk sudo python setup.py install
Create project, which be called 'secure_site':
django-admin.py startproject secure_site
Test the installation - start our project:
cd secure_site/ chmod +x manage.py ./manage.py runserver 9000
Open browser by URL: http://localhost:9000/ and if 'It worked!' page is shown, then go further.
Create two applications - sample (for testing) and secure (for handling security information):
./manage.py startapp sample ./manage.py startapp secure
Now change settings.py file to use SQLite database:
DATABASE_ENGINE = 'sqlite3' DATABASE_NAME = 'secure_site.db'
Also activate admin interface via adding 'django.contrib.admin' to the end of INSTALLED_APPS in settings.py and uncomment lines in urls.py:
from django.conf.urls.defaults import * # Uncomment the next two lines to enable the admin: from django.contrib import admin admin.autodiscover() urlpatterns = patterns('', # Example: # (r'^secure_site/', include('secure_site.foo.urls')), # Uncomment the next line to enable admin documentation: (r'^admin/doc/', include('django.contrib.admindocs.urls')), # Uncomment the next line for to enable the admin: (r'^admin/(.*)', admin.site.root), )
Now it is time to create database and superuser account:
./manage.py syncdb
Open browser by URL: http://localhost:9000/admin/ and if 'Login' page is shown, try to login with login/password created during syncdb and if everything is fine then go further.
Now it is time to create sample models. Open sample/models.py with your favourite editor and add two models:
""" Sample models. """ from django.db import models class Model1(models.Model): """ Sample model 1. """ name = models.CharField(max_length = '100') def __unicode__(self): """ String representation of this model. """ return self.name class Model2(models.Model): """ Sample model 2. """ name = models.CharField(max_length = '100') def __unicode__(self): """ String representation of this model. """ return self.name
Also it is required to register these model with admin site. Just create sample/admin.py file and add these lines:
from django.contrib import admin from secure_site.sample.models import Model1, Model2 admin.site.register(Model1) admin.site.register(Model2)
After that register models in settings.py (add 'secure_site.sample' to INSTALLED_APPS) and synchronize database.
Run server and open browser by URL: http://localhost:9000/admin/ and if you see links to models 1 and 2 after logging in then go further. Now we have to create model for securing our application. This model will contains application and model names and users' names who do not have access to modify objects. Open secure/models.py and edit it with favourite editor:
""" Models for securing Django project. """ from django.db import models from django.conf import settings from django.contrib.auth.models import User import re def get_apps_list(): """ Get list of available applications. """ app_regex = re.compile('secure_site\.(\w+)') apps = [re.search(app_regex, app).group(1) for app in settings.INSTALLED_APPS if app.startswith('secure_site.')] result = [] for app in apps: try: __import__('%s.models' % app, fromlist = [app]) result.append((app, app)) except ImportError: pass return result class SecuredModule(models.Model): """ Model for module. """ app = models.CharField(max_length = '100', choices = get_apps_list()) model = models.CharField(max_length = '100', choices = [('', '')]) readers = models.ManyToManyField(User, blank = True) def __unicode__(self): return '%s.%s' % (self.app.lower(), self.model.lower())
Register model with admin site (secure/admin.py):
from django.contrib import admin from secure_site.secure.models import SecuredModule admin.site.register(SecuredModule)
After that register models in settings.py (add 'secure_site.secure' to INSTALLED_APPS) and synchronize database.
Now you can see new application on the admin page - Secure. If you will try to add new module you will see all available application (in our case - secure and sample) and all registered users. But there is an issue here - the list of choices for models is empty. Now we will fix it using AJAX with jQuery. First of all, you have to download jQuery from official site and put it to you application. For this we will create media dir and register it in the project:
mkdir -p media.site/js cd media.site/js wget http://jqueryjs.googlecode.com/files/jquery-1.2.6.min.js ln -s jquery-1.2.6.min.js jquery.js cd -
Now it is required to set up MEDIA_ROOT (settings.py)
import os CURRENT_PATH = os.path.dirname(__file__) MEDIA_ROOT = os.path.join(CURRENT_PATH, 'media.site')
And fix urls.py to view media files (add this line to urlpatterns):
(r'^media.site/(?P.*)$', 'django.views.static.serve', {'document_root': settings.MEDIA_ROOT}),
Now it is time to modify admin's change_form template for SecuredModule model and include jQuery script. For this we create templates dir, register it and modify required template.
mkdir templates
Modify settins.py (CURRENT_PATH was declared earlier during setting up MEDIA_ROOT):
TEMPLATE_DIRS = ( os.path.join(CURRENT_PATH, 'templates'), )
Prepare template for change_form:
mkdir -p templates/admin/secure/securedmodule touch templates/admin/secure/securedmodule/change_form.html
Now let's modify change_form.html:
{% extends "admin/change_form.html" %} {% block extrahead %} {{ block.super }} <script type="text/javascript" src="/media.site/jquery.js"></script> <script type="text/javascript"> $(document).ready(function() { var app = $('select#id_app'); var reload_func = function() { $.getJSON('/get_models/', {app : app.val()}, function(j) { var options = ''; var model = ''; $.each(j, function(i, item) { if (item.model) { model = item.model; } if (item.value) { options += ''; } }); $('select#id_model').html(options); }); }; reload_func(); app.change(reload_func); }); </script> {% endblock %}
In code above we call '/get_models/' request handler via AJAX-request and modify id_model options. Let's add get_models view (change secure/views.py):
""" Views for working with secure application. """ import re from secure_site.secure.models import SecuredModule from django.http import HttpResponse, Http404 from django.utils import simplejson import logging from django.db import models class JsonResponse(HttpResponse): """ Class representing JSON response. """ def __init__(self, data): HttpResponse.__init__(self, content = simplejson.dumps(data), mimetype = 'application/json') MODULE_ID_REGEX = re.compile('/admin/secure/securedmodule/(\d+)/?$') def get_app_module(app): """ Get application module. 'app' - application name.""" try: return __import__('%s.models' % app, fromlist = [app]) except ImportError, error: logging.debug(error) raise Http404 def get_module_classes(app): """ Get module classes. 'app' - application name.""" module_values = get_app_module(app).__dict__.values() return [val for val in module_values if isinstance(val, type) and issubclass(val, models.Model)] def get_models(request): """ Get all available models. 'request' - request instance.""" app = request.GET.get('app', '') reply = [] matches = MODULE_ID_REGEX.search(request.META['HTTP_REFERER']) if matches: # module is existed id = matches.group(1) module = SecuredModule.objects.get(id = id) reply.append({'model' : module.model}) if app: classes = get_module_classes(app) reply.extend([{ 'value': class_obj.__name__} for class_obj in classes]); SecuredModule._meta.get_field('model')._choices.extend( [(class_obj.__name__, class_obj.__name__) for class_obj in classes]) return JsonResponse(reply)
We defined JsonResponse class for working with AJAX and added get_models view. Register it in urls.py:
from django.conf.urls.defaults import * from django.contrib import admin from django.conf import settings from secure_site.secure.views import get_models admin.autodiscover() urlpatterns = patterns('', (r'^get_models/?$', get_models), (r'^media.site/(?P.*)$', 'django.views.static.serve', {'document_root': settings.MEDIA_ROOT}), (r'^admin/doc/', include('django.contrib.admindocs.urls')), (r'^admin/(.*)', admin.site.root), )
Now everything is ready to dynamically refresh model's field in form. After starting project with runserver do this:
- - add new user 'test' with Staff and Superuser status;
- - add new module for model1 and test user as reader;
- - add few model1 objects.
And the last part - checking user's access. The one of ways is using middleware. It is not best way (we can't restrict to change model via Django API), but acceptable enough for the demo. Let's create middleware class:
mkdir middleware touch middleware/secure.py touch middleware/__init__.py
Add 'secure_site.middleware.secure.CheckAccess' to the end of MIDDLEWARE_CLASSES list in settings.py. And change middleware/secure.py:
import re from django.core.exceptions import ObjectDoesNotExist from django.http import HttpResponseForbidden from secure_site.secure.models import SecuredModule APP_MODEL_REGEX = re.compile('^/admin/(\w+)/(\w+)/\d+/?$') def find_module(app, model): """ Find module object using application and model names. 'app' - application name. 'model' - model name.""" module_name = '%s.%s' % (app, model) for module in SecuredModule.objects.all(): if module_name in module.__unicode__(): return module return None class CheckAccess(object): """Middleware that gets various objects from the request object and saves them in thread local storage.""" def process_request(self, request): matches = APP_MODEL_REGEX.search(request.META['PATH_INFO']) if matches: app = matches.group(1) model = matches.group(2) module = find_module(app, model) if module: try: module.readers.get(username = unicode(getattr(request, 'user', None))) return HttpResponseForbidden('That's all! Now let's test it: login with test user and try to open any of model1 objects created earlier. If you got 'Permission denied!' message, then everything is fine - security features are working!Permission denied!
') except ObjectDoesNotExist: pass
DOWNLOAD - secure_site.tar.gz (20.58KB)
Comments
Post a Comment