Cookie Consent by Free Privacy Policy Generator ๐Ÿ“Œ Instance version control in DRF with Django Reversion

๐Ÿ  Team IT Security News

TSecurity.de ist eine Online-Plattform, die sich auf die Bereitstellung von Informationen,alle 15 Minuten neuste Nachrichten, Bildungsressourcen und Dienstleistungen rund um das Thema IT-Sicherheit spezialisiert hat.
Ob es sich um aktuelle Nachrichten, Fachartikel, Blogbeitrรคge, Webinare, Tutorials, oder Tipps & Tricks handelt, TSecurity.de bietet seinen Nutzern einen umfassenden รœberblick รผber die wichtigsten Aspekte der IT-Sicherheit in einer sich stรคndig verรคndernden digitalen Welt.

16.12.2023 - TIP: Wer den Cookie Consent Banner akzeptiert, kann z.B. von Englisch nach Deutsch รผbersetzen, erst Englisch auswรคhlen dann wieder Deutsch!

Google Android Playstore Download Button fรผr Team IT Security



๐Ÿ“š Instance version control in DRF with Django Reversion


๐Ÿ’ก Newskategorie: Programmierung
๐Ÿ”— Quelle: dev.to

Instance Logging

Instance logging or Model logging is a widely used practice that stores all instance updates of a model, it ensures that each instance within that particular model has it's own version history that can be tracked as the instance undergoes several updates.

Consider a simple blog application where a blog is an instance of the blog model, when interacting with this blog instance, we retrieve the current blog's attributes, however, this blog instance might have been updated several times until it has reached its current state, in order to ensure the completeness of the instance's data, we would need to save all the past states of the instance since its creation, it would also make sense to include some metadata for each logged state, including the system user that has performed the update, and a date-time indicating when this update was performed.

Django Reversion

Django Reversion is a powerful module that allows us to keep track of an instance's version history, it also provides several operations that can be used to create "version control" like functionality for each and every instance of any model, including reverting an instance to an earlier state.

Tutorial

In this article we will create a simple blog application with DRF and integrate Django Reversion, we will create corresponding views which will be responsible retrieving an instance's version history and reverting the instance to an earlier state.

First, let's create a simple DRF application, we will start by defining a simple Blog model inside our models.py file of our blog app.

blog/models.py

from django.db import models


class Blog(models.Model):

    title = models.CharField(max_length=50, help_text="A title for the blog instance.")

    description = models.CharField(
        max_length=150, null=True, help_text="An optional short description of the blog instance."
    )

    content = models.TextField(help_text="The blog instance's content body.")

Next, we will create a serializer for our blog app that will be used for simple CRUD operations, for this application, here we will use DRF's ModelSerializer, this will be done inside our serializers.py file.

blog/serializers.py

from rest_framework import serializers
from blog.models import Blog


class BlogSerializer(serializers.ModelSerializer):

    class Meta:
        model = Blog
        fields = '__all__'

Now let's define a view inside our views.py file, that will handle different operations on the Blog model, in this example we will use DRF's GenericViewSet and add Mixins based on our needs.

blog/views.py

from rest_framework.viewsets import GenericViewSet
from rest_framework.mixins import CreateModelMixin, UpdateModelMixin, RetrieveModelMixin, ListModelMixin
from blog.models import Blog
from blog.serializers import BlogSerializer


class BlogViewSet(UpdateModelMixin, RetrieveModelMixin, ListModelMixin, CreateModelMixin, GenericViewSet):

    queryset = Blog.objects.all()
    serializer_class = BlogSerializer

After that we just need to register our ViewSet inside our urls.py file.

blog/urls.py

from .views import BlogViewSet
from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register('blog', BlogViewSet, basename='blog')
url_patterns = router.urls

Make sure to import and add all the urlpatterns of the blog app inside the main urls.py file as well.

djangoProject/urls.py

from blog.urls import url_patterns as blog_url_patterns

urlpatterns = []
urlpatterns += blog_url_patterns

Now that everything is set up, we are ready to implement the Django Reversion module into our app.

First, we have to install the Django Reversion module.

  1. Install the Django Reversion module with the following command.
    pip install django-reversion

  2. Add reversion to INSTALLED_APPS of your settings.py

  3. Migrate your application with python manage.py migrate

Now that we have installed Django Reversion, we are going to register our Blog model for reversion.

  1. Inside our models.py file, import the register decorator from the reversion module.

  2. Add the decorator on top of our model class.

The updated models.py file should look like this.

blog/models.py

from django.db import models
from reversion import register


@register()
class Blog(models.Model):

    title = models.CharField(max_length=50, help_text="A title for the blog instance.")

    description = models.CharField(
        max_length=150, null=True, help_text="An optional short description of the blog instance."
    )

    content = models.TextField(help_text="The blog instance's content body.")

Next, we are going to implement the reversion module into our viewset, there are many ways for us to do that, including using the context API manually, using the reversion decorator, or using the reversion view mixin, in this tutorial I will be using the mixin.

The RevisionMixin will automatically create revisions for each update inside our viewset class, which is great, but we want to be able to retrieve the version history of each instance, also we want to be able to revert the instance to an older version.

Therefore we will be overriding the RevisionMixin, and adding two actions to it, one for retrieving a list of older versions, and one for reverting the instance to an older version.

Let's create a customized version of the RevisionMixin, that can be reused for any viewset, by simply adding it to the list of inherited classes.

  1. Create a new directory called revision inside our blog directory.

  2. Add a views.py file to the newly created directory.

  3. Inside this file, we import the RevisionMixin from reversion.views.

  4. Create a new class, that is going to be our customized version of the mixin, I'm going to name it CustomRevisionMixin.

  5. Add the reversion module's RevisionMixin inside the CustomRevisionMixin parameters, in order to inherit the current reversion module's functionality.

blog/revision/views.py

from reversion.views import RevisionMixin
class CustomRevisionMixin(RevisionMixin):
    pass

Now we can add our own functionality, we are going to create the two extra actions here.

Import the action decorator from rest_framework.decorators.

from rest_framework.decorators import action

Create an action for retrieving the instance's logs, with the methods parameter set to ['GET'], and detail parameter set to True.

@action(methods=['GET'], detail=True)
def instance_logs(self, request, **kwargs):
    pass

Next, let's create a function that will retrieve the current instance's version history, we will call the function get_versions.

  1. Import the Version model from reversion.models.

  2. Import the RegistrationError exception class from reversion.errors.

  3. Retrieve the current instance by calling the viewset's get_object method.

  4. Call the get_for_object method of the Version model's manager, since in the docs it is stated that this method might throw a RegistrationError if called for an unregistered model, we are going to wrap it inside a try except clause, and throw a standard APIException (from rest_framework.exceptions) for this case.

  5. Filter the queryset by taking out the last version object, since it's ordered in a descending matter, this object will be the current instance's version, which we won't need in our version history.

  6. Return the instance and the version queryset as a tuple.

def get_versions(self):
    instance = self.get_object()
    try:
        versions = Version.objects.get_for_object(instance)
    except RegistrationError:
        raise APIException(detail='model has not been registered for revision.')
    current_version = versions[0]
    versions = versions.exclude(pk=current_version.id)
    return instance, versions

The next step is to create a serializer for the Version model, as usual, we will be using a ModelSerializer to achieve this.

  1. Create a serializers.py file inside the revision directory.

  2. Import serializers from rest_framework, and the Version model from reversion.models.

  3. Define a serializer with the model set to Version and define fields based on the available data on the Version model, for reference check the source code or documentation of the Django Reversion module, for this tutorial, I will be adding the following four fields:

  • version, primary key of the version instance.
  • updated_at, creation date of the version instance, which simultaneously indicates when the instance was updated.
  • updated_by, user id of the user that has performed the update, if authentication is implemented.
  • instance, a JSON object representation of the instance after the update.

Your serializer class should look like this.

blog/revision/serializers.py

from rest_framework import serializers
from reversion.models import Version


class RevisionSerializer(serializers.ModelSerializer):
    version = serializers.PrimaryKeyRelatedField(read_only=True, source='id')
    updated_at = serializers.DateTimeField(read_only=True, source='revision.date_created')
    updated_by = serializers.PrimaryKeyRelatedField(read_only=True, source='revision.user')
    instance = serializers.JSONField(read_only=True, source='field_dict')

    class Meta:
        model = Version
        fields = ['version', 'updated_at', 'updated_by', 'instance']

Now we finalize our instance_logs action.

  1. Import the newly created RevisionSerializer into our views.py file.

  2. Import Response from rest_framework.response.

  3. Call the get_versions function.

  4. Pass the versions as the serializer instance, don't forget to add many=True kwarg.

  5. Return the serialized data.

@action(methods=['GET'], detail=True)
def instance_logs(self, request, **kwargs):
    instance, versions = self.get_versions()
    serializer = RevisionSerializer(versions, many=True)
    return Response(serializer.data)

Now we define our second action, which will handle the reversion of an instance to an older version.

  1. Create a revert_instance function and add the action decorator on top of it.

  2. The method for this action will be POST, detail should be set to True.

  3. Call the get_versions function.

  4. Define a second serializer called RevisionRevertSerializer which overrides the RevisionSerializer inside our serializers.py file.

  5. Override the __init__ function, to dynamically define the version field, this is needed because we want to pass the versions queryset as a context, in order to prevent the user from sending invalid version ids.

  6. Override the serializer's update function to extract the version instance from the validated_data.

  7. Call the revert method of the revision instance API, first, access the revision instance from the extracted version instance with dot notation, then, call the revert method.

  8. Since revert might throw a RevertError in case the database schema has been changed, we are going to wrap it inside a try except clause and throw a custom message for handling this case.

  9. Import the RevisionRevertSerializer serializer inside out views.py file.

  10. Validate and save the serializer, then return a success message as response.

The final code should look like this.

blog/revision/views.py

from reversion.models import Version
from reversion.views import RevisionMixin
from reversion.errors import RegistrationError
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.exceptions import APIException
from blog.revision.serializers import RevisionSerializer, RevisionRevertSerializer


class CustomRevisionMixin(RevisionMixin):

    def get_versions(self):
        instance = self.get_object()
        try:
            versions = Version.objects.get_for_object(instance)
        except RegistrationError:
            raise APIException(detail='model has not been registered for revision.')
        current_version = versions[0]
        versions = versions.exclude(pk=current_version.id)
        return instance, versions

    @action(methods=['GET'], detail=True)
    def instance_logs(self, request, **kwargs):
        instance, versions = self.get_versions()
        serializer = RevisionSerializer(versions, many=True)
        return Response(serializer.data)

    @action(methods=['POST'], detail=True)
    def revert_instance(self, request, **kwargs):
        instance, versions = self.get_versions()
        serializer = RevisionRevertSerializer(instance, data=request.data, context={'versions': versions})
        serializer.is_valid(raise_exception=True)
        serializer.save()
        version = serializer.validated_data.get('version')
        return Response({'message': 'instance reverted to version %d.' % version.id})

blog/revision/serializers.py

from rest_framework import serializers
from rest_framework.exceptions import APIException
from reversion.models import Version
from reversion.errors import RevertError


class RevisionSerializer(serializers.ModelSerializer):
    version = serializers.PrimaryKeyRelatedField(read_only=True, source='id')
    updated_at = serializers.DateTimeField(read_only=True, source='revision.date_created')
    updated_by = serializers.PrimaryKeyRelatedField(read_only=True, source='revision.user')
    instance = serializers.JSONField(read_only=True, source='field_dict')

    class Meta:
        model = Version
        fields = ['version', 'updated_at', 'updated_by', 'instance']


class RevisionRevertSerializer(RevisionSerializer):

    def __init__(self, *args, **kwargs):
        super(RevisionSerializer, self).__init__(*args, **kwargs)
        version_queryset = self.context.get('versions')
        self.fields['version'] = serializers.PrimaryKeyRelatedField(write_only=True, queryset=version_queryset)

    def update(self, instance, validated_data):
        version = validated_data['version']
        try:
            version.revision.revert()
            return validated_data
        except RevertError:
            raise APIException(detail='can not revert instance.')

    class Meta(RevisionSerializer.Meta):
        fields = ['version']

Finally, we can add this custom revision mixin to any viewset and it will automatically create revisions for any instance update, and receive both custom actions, instance_logs and revert_instance.

  1. Inside our blog/views.py file import the CustomRevisionMixin.

  2. Add the mixin to be inherited by the viewset.

blog/views.py

from rest_framework.viewsets import GenericViewSet
from rest_framework.mixins import CreateModelMixin, UpdateModelMixin, RetrieveModelMixin, ListModelMixin
from blog.models import Blog
from blog.serializers import BlogSerializer
from blog.revision.views import CustomRevisionMixin


class BlogViewSet(CustomRevisionMixin, UpdateModelMixin, RetrieveModelMixin, ListModelMixin, CreateModelMixin, GenericViewSet):

    queryset = Blog.objects.all()
    serializer_class = BlogSerializer

All done! Now we can test our code.

First, let's create a blog instance.

METHOD: POST 
URL: /blog/
BODY: {
    "title": "version 1 title",
    "description": "version 1 description",
    "content": "version 1 content"
}

Next, let's update our created instance.

METHOD: PATCH
URL: /blog/1/
BODY: {
    "title": "version 2 title",
    "description": "version 2 description",
    "content": "version 2 content"
}

After that, we are going to check the instance logs.

METHOD: GET
URL: /blog/1/instance_logs/

Our response should contain the older version (prior to update).

[
    {
        "version": 1,
        "updated_at": "2023-04-22T22:55:29.235430Z",
        "updated_by": null,
        "instance": {
            "id": 1,
            "title": "version 1 title",
            "description": "version 1 description",
            "content": "version 1 content"
        }
    }
]

Here we can see that the version id is 1, so we have to provide {"version": 1}, to the revert_instance endpoint in order to revert it.

METHOD: POST
URL: /blog/1/revert_instance/
BODY: {
    "version": 1
}

Now let's check if the instance has been reverted by retrieving the instance.

METHOD: GET
URL: /blog/1/

As expected, the instance has been successfully reverted.

{
    "id": 1,
    "title": "version 1 title",
    "description": "version 1 description",
    "content": "version 1 content"
}

And another version has been added to the instance's logs.

[
    {
        "version": 2,
        "updated_at": "2023-04-22T23:00:44.489784Z",
        "updated_by": null,
        "instance": {
            "id": 1,
            "title": "version 2 title",
            "description": "version 2 description",
            "content": "version 2 content"
        }
    },
    {
        "version": 1,
        "updated_at": "2023-04-22T22:55:29.235430Z",
        "updated_by": null,
        "instance": {
            "id": 1,
            "title": "version 1 title",
            "description": "version 1 description",
            "content": "version 1 content"
        }
    }
]

That's all for this article, I really hope it was helpful.

Source Code: https://github.com/saruar999/django-reversion-with-drf

...



๐Ÿ“Œ Instance version control in DRF with Django Reversion


๐Ÿ“ˆ 110.79 Punkte

๐Ÿ“Œ CVE-2018-25045 | Django REST Framework up to 3.9.0 DRF Browsable API cross site scripting


๐Ÿ“ˆ 45.77 Punkte

๐Ÿ“Œ DRF Luftrettung: So kรผndigen Sie Ihre Mitgliedschaft


๐Ÿ“ˆ 33.09 Punkte

๐Ÿ“Œ Omise: Found Origin IP's Lead To Access To [ Grafana Instance , PgHero Instance [ Can SQL Injection ]


๐Ÿ“ˆ 32.23 Punkte

๐Ÿ“Œ Instance Discovery, Agent Install, and Configuration Management with Instance Manager


๐Ÿ“ˆ 32.23 Punkte

๐Ÿ“Œ Vuln: Django 'django.views.static.serve()' Function Open Redirection Vulnerability


๐Ÿ“ˆ 25.37 Punkte

๐Ÿ“Œ Vuln: Django 'django.contrib.auth.views.login()' Function Open Redirection Vulnerability


๐Ÿ“ˆ 25.37 Punkte

๐Ÿ“Œ Django up to 1.8.17/1.9.12/1.10.6 django.views.static.serve Open Redirect


๐Ÿ“ˆ 25.37 Punkte

๐Ÿ“Œ Django up to 1.8.17/1.9.12/1.10.6 django.utils.http.is_safe_url cross site scripting


๐Ÿ“ˆ 25.37 Punkte

๐Ÿ“Œ Django up to 2.0.1 django.contrib.auth.forms.AuthenticationForm confirm_login_allowed information disclosure


๐Ÿ“ˆ 25.37 Punkte

๐Ÿ“Œ Django 1.8.1/1.8.2/1.8.6/1.9 django.utils.http.is_safe_url() Cross Site Scripting


๐Ÿ“ˆ 25.37 Punkte

๐Ÿ“Œ Django up to 1.11.14/2.0.7 django.middleware.common.CommonMiddleware Open Redirect


๐Ÿ“ˆ 25.37 Punkte

๐Ÿ“Œ Medium CVE-2019-10682: Django-nopassword project Django-nopassword


๐Ÿ“ˆ 25.37 Punkte

๐Ÿ“Œ Django up to 1.11.18/2.0.10/2.1.5 django.utils.numberformat.format() denial of service


๐Ÿ“ˆ 25.37 Punkte

๐Ÿ“Œ Django up to 2.2.17/3.0.11/3.1.5 Archive django.utils.archive.extract path traversal


๐Ÿ“ˆ 25.37 Punkte

๐Ÿ“Œ Django 1.8.1/1.8.2/1.8.6/1.9 django.utils.http.is_safe_url() Cross Site Scripting


๐Ÿ“ˆ 25.37 Punkte

๐Ÿ“Œ Low CVE-2021-21416: Django-registration project Django-registration


๐Ÿ“ˆ 25.37 Punkte

๐Ÿ“Œ django-registration up to 3.1.1 on Django User Registration information exposure


๐Ÿ“ˆ 25.37 Punkte

๐Ÿ“Œ Django 1.4 up to 1.7 Redirect django.util.http.is_safe_url input validation


๐Ÿ“ˆ 25.37 Punkte

๐Ÿ“Œ Django bis 1.8.17/1.9.12/1.10.6 django.views.static.serve() erweiterte Rechte


๐Ÿ“ˆ 25.37 Punkte

๐Ÿ“Œ Is Django Unchained on Netflix? How to Watch Django Unchained from Anywhere


๐Ÿ“ˆ 25.37 Punkte

๐Ÿ“Œ CVE-2015-8213 | Django CMS 1.7.10/1.8.6 Date Template Filter django.utils.formats.get_format information disclosure (USN-2816-1 / BID-77750)


๐Ÿ“ˆ 25.37 Punkte

๐Ÿ“Œ CVE-2016-2512 | Django 1.8.1/1.8.2/1.8.6/1.9 django.utils.http.is_safe_url cross site scripting (USN-2915-1 / BID-83879)


๐Ÿ“ˆ 25.37 Punkte

๐Ÿ“Œ Migrating From Django to Next.js: Whatโ€™s the Equivalent for Django-Guardian?


๐Ÿ“ˆ 25.37 Punkte

๐Ÿ“Œ FullStack Next.js & Django Authentication: Django REST, TypeScript, JWT, Wretch & Djoser


๐Ÿ“ˆ 25.37 Punkte

๐Ÿ“Œ Django bis 1.8.17/1.9.12/1.10.6 django.utils.http.is_safe_url() Cross Site Scripting


๐Ÿ“ˆ 25.37 Punkte

๐Ÿ“Œ Django bis 2.0.1 django.contrib.auth.forms.AuthenticationForm confirm_login_allowed Information Disclosure


๐Ÿ“ˆ 25.37 Punkte

๐Ÿ“Œ Django bis 1.11.14/2.0.7 django.middleware.common.CommonMiddleware Open Redirect


๐Ÿ“ˆ 25.37 Punkte

๐Ÿ“Œ Django News #176 - Django 5.x Technical Board Election Registration


๐Ÿ“ˆ 25.37 Punkte

๐Ÿ“Œ Build a Multivendor E-commerce Website using Django, React & Django Rest Framework


๐Ÿ“ˆ 25.37 Punkte

๐Ÿ“Œ How to Use Role-Based Access Control with Instance Manager


๐Ÿ“ˆ 21.58 Punkte

๐Ÿ“Œ ExchangeFinder - Find Microsoft Exchange Instance For A Given Domain And Identify The Exact Version


๐Ÿ“ˆ 20.74 Punkte

๐Ÿ“Œ Django up to 2.2.23/3.1.11/3.2.3 access control [CVE-2021-33571]


๐Ÿ“ˆ 18.16 Punkte

๐Ÿ“Œ CVE-2016-2048 | Django 1.9.0/1.9.1 Change Permission save_as=True access control (BID-82329 / ID 89009)


๐Ÿ“ˆ 18.16 Punkte

๐Ÿ“Œ Implementing Role-Based Access Control in Django


๐Ÿ“ˆ 18.16 Punkte











matomo