Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add token based authentication feature #326

Merged
merged 13 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions REQUIREMENTS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ django-bootstrap-pagination==1.7.1
django-sortable-listview==0.43
django-user-map
djangorestframework==3.12.2
pyjwt==1.7.1
djangorestframework-simplejwt==4.4
django-rest-auth==0.9.5
drf-yasg
django-matomo==0.1.6
3 changes: 3 additions & 0 deletions dockerize/docker/REQUIREMENTS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ requests==2.23.0
markdown==3.2.1

djangorestframework==3.11.2
pyjwt==1.7.1
djangorestframework-simplejwt==4.4

sorl-thumbnail-serializer-field==0.2.1
django-rest-auth==0.9.5
drf-yasg==1.17.1
Expand Down
39 changes: 39 additions & 0 deletions qgis-app/plugins/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from functools import wraps
from django.http import HttpResponseForbidden
from rest_framework_simplejwt.authentication import JWTAuthentication
from plugins.models import Plugin, PluginOutstandingToken
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError
from rest_framework_simplejwt.token_blacklist.models import BlacklistedToken, OutstandingToken
import datetime

def has_valid_token(function):
@wraps(function)
def wrap(request, *args, **kwargs):
auth_token = request.META.get("HTTP_AUTHORIZATION")
package_name = kwargs.get('package_name')
if not str(auth_token).startswith('Bearer'):
raise InvalidToken("Invalid token")

# Validate JWT token
authentication = JWTAuthentication()
try:
validated_token = authentication.get_validated_token(auth_token[7:])
plugin_id = validated_token.payload.get('plugin_id')
jti = validated_token.payload.get('refresh_jti')
token_id = OutstandingToken.objects.get(jti=jti).pk
is_blacklisted = BlacklistedToken.objects.filter(token_id=token_id).exists()
if not plugin_id or is_blacklisted:
raise InvalidToken("Invalid token")

plugin = Plugin.objects.get(pk=plugin_id)
if not plugin or plugin.package_name != package_name:
raise InvalidToken("Invalid token")
plugin_token = PluginOutstandingToken.objects.get(token__pk=token_id, plugin=plugin)
plugin_token.last_used_on = datetime.datetime.now()
plugin_token.save()
request.plugin_token = plugin_token
return function(request, *args, **kwargs)
except (InvalidToken, TokenError) as e:
return HttpResponseForbidden(str(e))

return wrap
13 changes: 12 additions & 1 deletion qgis-app/plugins/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from django.forms import CharField, ModelForm, ValidationError
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from plugins.models import Plugin, PluginVersion, PluginVersionFeedback
from plugins.models import Plugin, PluginOutstandingToken, PluginVersion, PluginVersionFeedback
from plugins.validator import validator
from taggit.forms import *

Expand Down Expand Up @@ -259,3 +259,14 @@ def clean(self):
self.cleaned_data['tasks'] = tasks

return self.cleaned_data

class PluginTokenForm(ModelForm):
"""
Form for token description editing
"""

class Meta:
model = PluginOutstandingToken
fields = (
"description",
)
6 changes: 2 additions & 4 deletions qgis-app/plugins/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
# Author: A. Pasotti

from django.contrib import auth
from django.contrib.auth.models import User

from rest_framework_simplejwt.authentication import JWTAuthentication

def HttpAuthMiddleware(get_response):
"""
Expand All @@ -12,7 +11,7 @@ def HttpAuthMiddleware(get_response):

def middleware(request):
auth_basic = request.META.get("HTTP_AUTHORIZATION")
if auth_basic:
if auth_basic and not str(auth_basic).startswith('Bearer'):
import base64

username, dummy, password = base64.decodestring(
Expand All @@ -27,7 +26,6 @@ def middleware(request):
# by logging the user in.
request.user = user
auth.login(request, user)

response = get_response(request)

# Code to be executed for each request/response after
Expand Down
24 changes: 24 additions & 0 deletions qgis-app/plugins/migrations/0005_pluginoutstandingtoken.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 2.2.25 on 2023-12-11 23:36

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('token_blacklist', '0007_auto_20171017_2214'),
('plugins', '0004_merge_20231122_0223'),
]

operations = [
migrations.CreateModel(
name='PluginOutstandingToken',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_blacklisted', models.BooleanField(default=False)),
('plugin', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='plugins.Plugin')),
('token', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='token_blacklist.OutstandingToken')),
],
),
]
28 changes: 28 additions & 0 deletions qgis-app/plugins/migrations/0006_auto_20231218_0225.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 2.2.25 on 2023-12-18 02:25

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('plugins', '0005_pluginoutstandingtoken'),
]

operations = [
migrations.AddField(
model_name='pluginoutstandingtoken',
name='description',
field=models.CharField(blank=True, help_text="Describe this token so that it's easier to remember where you're using it.", max_length=512, null=True, verbose_name='Description'),
),
migrations.AddField(
model_name='pluginoutstandingtoken',
name='is_newly_created',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='pluginoutstandingtoken',
name='last_used_on',
field=models.DateTimeField(blank=True, null=True, verbose_name='Last used on'),
),
]
30 changes: 30 additions & 0 deletions qgis-app/plugins/migrations/0007_auto_20240109_0428.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Generated by Django 2.2.25 on 2024-01-09 04:28

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('plugins', '0006_auto_20231218_0225'),
]

operations = [
migrations.AddField(
model_name='pluginversion',
name='is_from_token',
field=models.BooleanField(default=False, verbose_name='Is uploaded using token'),
),
migrations.AddField(
model_name='pluginversion',
name='token',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='plugins.PluginOutstandingToken', verbose_name='Token used'),
),
migrations.AlterField(
model_name='pluginversion',
name='created_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Created by'),
),
]
38 changes: 37 additions & 1 deletion qgis-app/plugins/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from django.utils import timezone
from djangoratings.fields import AnonymousRatingField
from taggit_autosuggest.managers import TaggableManager
from rest_framework_simplejwt.token_blacklist.models import OutstandingToken

PLUGINS_STORAGE_PATH = getattr(settings, "PLUGINS_STORAGE_PATH", "packages/%Y")
PLUGINS_FRESH_DAYS = getattr(settings, "PLUGINS_FRESH_DAYS", 30)
Expand Down Expand Up @@ -671,6 +672,33 @@ def from_db_value(self, value, expression, connection):
return self.to_python(value)


class PluginOutstandingToken(models.Model):
"""
Plugin outstanding token
"""
plugin = models.ForeignKey(
Plugin,
on_delete=models.CASCADE
)
token = models.ForeignKey(
OutstandingToken,
on_delete=models.CASCADE
)
is_blacklisted = models.BooleanField(default=False)
is_newly_created = models.BooleanField(default=False)
description = models.CharField(
verbose_name=_("Description"),
help_text=_("Describe this token so that it's easier to remember where you're using it."),
max_length=512,
blank=True,
null=True,
)
last_used_on = models.DateTimeField(
verbose_name=_("Last used on"),
blank=True,
null=True
)

class PluginVersion(models.Model):
"""
Plugin versions
Expand All @@ -686,7 +714,7 @@ class PluginVersion(models.Model):
downloads = models.IntegerField(_("Downloads"), default=0, editable=False)
# owners
created_by = models.ForeignKey(
User, verbose_name=_("Created by"), on_delete=models.CASCADE
User, verbose_name=_("Created by"), on_delete=models.CASCADE, null=True, blank=True
)
# version info, the first should be read from plugin
min_qg_version = QGVersionZeroForcedField(
Expand Down Expand Up @@ -722,6 +750,14 @@ class PluginVersion(models.Model):
blank=False,
null=True,
)
is_from_token = models.BooleanField(
_("Is uploaded using token"),
default=False
)
# Link to the token if upload is using token
token = models.ForeignKey(
PluginOutstandingToken, verbose_name=_("Token used"), on_delete=models.CASCADE, null=True, blank=True
)

# Managers, used in xml output
objects = models.Manager()
Expand Down
5 changes: 5 additions & 0 deletions qgis-app/plugins/templates/plugins/plugin_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,11 @@ <h2>{{ object.name }}
<td>{{ version.min_qg_version }}</td>
<td>{{ version.max_qg_version }}</td>
<td>{{ version.downloads }}</td>
{% if version.is_from_token %}
<td>Token {{ version.token.description|default:"" }}</td>
{% else %}
<td><a href="{% url "user_details" version.created_by.username %}">{{ version.created_by }}</a></td>
{% endif %}
<td>{{ version.created_on|local_timezone }}</td>
{% if user.is_staff or user in version.plugin.approvers or user in version.plugin.editors %}<td><form method="post" action="{% url "version_manage" object.package_name version.version %}">{% csrf_token %}
{% if user.is_staff or user in version.plugin.approvers %}
Expand Down Expand Up @@ -270,6 +274,7 @@ <h2>{{ object.name }}
<div>
<a class="btn btn-primary" href="{% url "plugin_update" object.package_name %}">{% trans "Edit" %}</a>
<a class="btn btn-primary" href="{% url "version_create" object.package_name %}">{% trans "Add version" %}</a>
<a class="btn btn-primary" href="{% url "plugin_token_list" object.package_name %}">{% trans "Tokens" %}</a>
{% if user.is_staff %}
{% if object.featured %}<input class="btn btn-warning" type="submit" name="unset_featured" id="unset_featured" value="{% trans "Unset featured" %}" />
{% else %}<input class="btn btn-primary" type="submit" name="set_featured" id="set_featured" value="{% trans "Set featured" %}" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% extends 'plugins/plugin_base.html' %}{% load i18n %}
{% block content %}
<h3>Delete token of "{{ username }}"</h3>
<form action="" method="post">{% csrf_token %}
<p class="alert alert-danger">{% trans "You asked to delete a token.<br />The token will be permanently deleted and this action cannot be undone.<br />Please confirm." %}</p>
<p><input type="submit" class="btn btn-danger" name="delete_confirm" value="{% trans "Ok" %}" /> <a class="btn btn-default" href="javascript:history.back()">{% trans "Cancel" %}</a></p>
</form>

{% endblock %}
Loading
Loading