How to execute code from admin page in django

leondaz

Ahmed I. Elsayed

Posted on May 23, 2020

How to execute code from admin page in django

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

Execute button image

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
  1. Naive implementation, Without taking anything in consideration.
  2. Execute the code with Execute button on admin page.
  3. Security measures, What's the worst that can happen?
  4. 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.

Creating a Rule object from admin panel

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.

Grey box image

value is the text that will be shown on the button. We didn't include <form> because we're actually inside a form.

Source code image to prove that we're really in a form

Notice the area surrounding the button, this is submit-row in action.

Grey area around the button

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

Imgur

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.

💖 đŸ’Ș 🙅 đŸš©
leondaz
Ahmed I. Elsayed

Posted on May 23, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related