How to execute code from admin page in django
So I've been searching in stackoverflow a while ago and came across a nice question, The owner wants to have a button on the admin page to execute code that's dynamically generated. I've answered it there but I still want to emphasize how important this topic is and this is why I'm writing this right now.
This is what the execute code
button looks like
WE WILL NOT BE CREATING THE SAME THING FROM THE QUESTION, WE'LL BE CREATING SOMETHING MORE GENERIC THAT'S NOT QUESTION RELATED
This article will be divided into sections as follows
- Naive implementation, Without taking anything in consideration.
- Execute the code with
Execute
button on admin page. - Security measures, What's the worst that can happen?
- Best practices of dynamically generated code.
Let's get started.
Naive implementation
First of all, We're gonna have a model called Rule
and this Rule
model will have attributes 2 attributes, an attribute to hold the code itself and another for tracking if this was executed before.
So basically a Rule
is an executable object, It's just a fancy name, name it whatever you want.
This is how I've written the code
# models.py
class Rule(models.Model):
code = models.TextField()
was_executed_before = models.BooleanField(default=False) # it was not executed before.
Pretty much everything is self-explanatory, We want to do something at the row level, to the Rule
itself, this means we're gonna add a method to the Rule
model here, We'll call it execute
and when we call rule_obj.execute()
, This executes the code. Python has a builtin function eval(code: str)
that takes the code as a string and evaluates it, returns what the code have returned.
e.g
# ran in IDLE
x = eval('[1, 2, 3]')
x
# [1, 2, 3]
type(x)
# <class 'list'>
This is eval()
in a nutshell, back on topic, This is our model now
# models.py
class Rule(models.Model):
code = models.TextField()
was_executed_before = models.BooleanField(default=False) # it was not executed before.
def execute(self):
# omit return or leave it depending on your needs
return eval(self.code)
Let's talk a bit about this, Are we passing a TextField
object to eval? actually, if you're using PyCharm
IDE, the self.code
will be marked yellow, You can't pass a non-string in eval()
, This actually takes us to how repr()
and str()
work, the TextField
object is represented by it's value this is why this works and this is 100% correct, It's not a hack, It's the same as Rule.code = "Whatever"
, It's a string.
Let's register our model in the admin page first
# admin.py
....
from .models import Rule
admin.site.register(Rule)
Let's create a new Rule
object, btw you could have used python manage.py shell
to do this and this is totally fine but we need to add a button in the admin page anyway.
This is the Rule
object I've created, It's useless, Let's be sure of our logic, We'll override the save
method temporary to execute it after it gets saved.
add this to your Rule
model
def save(self, **kwargs):
super().save(**kwargs)
self.execute()
Now, Open the Rule
object you've created and click save without changing anything, This calls save
and you'll notice in the console you've Article written by LeOndaz
printed. Our naive implementation works.
Adding an execution button
Let's add a button to execute this rule when we click this button, To add a button, We'll override how django renders the change_form
, basically, the change_form
is the form in the images I used above, the form used in changing models data.
Overriding django templates is straight-forward, The path to override this template for a specific model is <app_name>/templates/admin/<app_name>/<model_name>/change_form.html
In my case, My app is called core
and my model is called Rule
so I'll use core/templates/admin/core/rule/change_form.html
This is the content I've.
{# this is core/templates/admin/core/rule/change_form.html #}
{% extends 'admin/change_form.html' %}
{% block submit_buttons_bottom %}
{{ block.super }} {# This calls super to get the default layout#}
{# now let's add a button underneath it #}
<div class="submit-row"> {# this button will POST request with the name 'execute' #}
<input type="submit" value="Execute the rule" name="execute">
</div>
{% endblock %}
NOTES: submit-row
class is a django builtin that they use in all of their buttons, This creates the grey area that contains the buttons like this.
value
is the text that will be shown on the button. We didn't include <form>
because we're actually inside a form.
Notice the area surrounding the button, this is submit-row
in action.
Let's add the functionality of this button using a ModelAdmin
# admin.py
from django.shortcuts import redirect
class RuleAdmin(admin.ModelAdmin):
def response_change(self, request, obj):
if "execute" in request.POST:
if not obj.was_executed_before:
try:
obj.execute()
obj.was_executed_before = True
obj.save()
except (ValueError, TypeError):
pass
return redirect(".")
# if we didn't find 'execute' in POST data
return super().response_change(request, obj)
# don't forget to register the RuleAdmin with Rule
admin.site.register(Rule, RuleAdmin)
Let's save, go to our button and click it.
[22/May/2020 23:26:01] "GET /admin/core/rule/1/change/ HTTP/1.1" 200 4751
[22/May/2020 23:26:02] "GET /admin/jsi18n/ HTTP/1.1" 200 3223
Article written by LeOndaz
[22/May/2020 23:26:04] "POST /admin/core/rule/1/change/ HTTP/1.1" 302 0
[22/May/2020 23:26:04] "GET /admin/core/rule/1/change/ HTTP/1.1" 200 4759
So it is actually working.
Security measures
I've added those to the top of my models.py
and pretty much any basic models.py
file will have those
from django.contrib.auth import get_user_model
User = get_user_model()
Now imagine owning a blog with moderators, One of those moderators can use Python, So he created this rule in the
and he got this output ['test']
So he actually can access everything on your server, He can get all of your users and change their password to something that he knows and bam, Your server is hacked! Now imagine if he's not a moderator, Someone who got access to the admin page, This is serious, This is why the implementation mentioned is only for demonstration purposes to make sure you get the idea.
Best practices of dynamically generated code
One of the best practices of creating dynamically generated code is to verify that this code matches a specific pattern, You don't want to prevent typing eval()
in the code, You want to allow typing something and prevent all others, This is why we'll use regex, We want to prevent running code that we don't like, including eval()
.
Say we want to allow print
and prevent eval
, we won't verify if eval()
is in the code, instead, we will verify if print()
is there using regex and if eval()
is found, It's automatically ignored.
Matching print statements, tested on regexr
print\("[a-zA-Z0-9]"\)
So this matches any print("whatever")
and it automatically ignores any other code, So instead of limiting the code to use, We limit it to the code we want to use, I hope this makes sense.
Now, in any file (but you'll have to import it) or in models.py
, add this function
# models.py
import re
valid_code_pattern = """print\("[a-zA-Z0-9]"\)"""
def valid_python_code(code):
if re.match(valid_code_pattern, code):
return True
return False
This will return True
if our code is valid with respect to how we defined the word Valid.
def execute(self):
if valid_python_code(self.code):
return eval(self.code)
else:
raise Exception
and this should only allow print
statements, This is much better and SAFER.
Final Thoughts
Here's the question I was talking about
So hopefully you've understood everything presented in this article, Feel free to say anything that can improve future articles but till then, stay safe.
Top comments (0)