add api_secret auth and (mostly) finalize firmware endpoint

This commit is contained in:
Laura Klünder 2023-11-05 19:09:36 +01:00
parent 9e9e41fb3f
commit aa2df8d3c5
6 changed files with 123 additions and 4 deletions

20
src/c3nav/api/auth.py Normal file
View file

@ -0,0 +1,20 @@
from django.utils.translation import gettext_lazy as _
from rest_framework.authentication import TokenAuthentication
from rest_framework.exceptions import AuthenticationFailed
class APISecretAuthentication(TokenAuthentication):
def authenticate_credentials(self, key):
from c3nav.control.models import UserPermissions
try:
user_perms = UserPermissions.objects.exclude(api_secret='').exclude(api_secret__isnull=True).filter(
api_secret=key
).get()
except UserPermissions.DoesNotExist:
raise AuthenticationFailed(_('Invalid token.'))
if not user_perms.user.is_active:
raise AuthenticationFailed(_('User inactive or deleted.'))
return (user_perms.user, user_perms)

View file

@ -76,7 +76,7 @@ def user_detail(request, user): # todo: make class based view
with transaction.atomic():
if api_secret_action in ('generate', 'regenerate'):
api_secret = get_random_string(64, string.ascii_letters+string.digits)
api_secret = '%d-%s' % (user.pk, get_random_string(64, string.ascii_letters+string.digits))
permissions.api_secret = api_secret
permissions.save()

91
src/c3nav/mesh/api.py Normal file
View file

@ -0,0 +1,91 @@
import hashlib
import json
from django.db import transaction
from rest_framework.authentication import SessionAuthentication
from rest_framework.exceptions import ParseError, PermissionDenied
from rest_framework.mixins import CreateModelMixin
from rest_framework.response import Response
from rest_framework.status import HTTP_201_CREATED
from rest_framework.viewsets import ReadOnlyModelViewSet
from c3nav.control.models import UserPermissions
from c3nav.mesh.messages import ChipType
from c3nav.mesh.models import FirmwareVersion
class FirmwareViewSet(CreateModelMixin, ReadOnlyModelViewSet):
"""
List and download firmwares, ordered by last update descending. Use ?offset= to specify an offset.
Don't forget to set X-Csrftoken for POST requests!
"""
queryset = FirmwareVersion.objects.all()
def get_queryset(self):
# todo: permissions
return FirmwareVersion.objects.all()
def _list(self, request, qs):
offset = 0
if 'offset' in request.GET:
if not request.GET['offset'].isdigit():
raise ParseError('offset has to be a positive integer.')
offset = int(request.GET['offset'])
return Response([obj.serialize() for obj in qs.order_by('-created')[offset:offset+20]])
def list(self, request, *args, **kwargs):
return self._list(request, self.get_queryset())
def create(self, request, *args, **kwargs):
# todo: this should probably be tested
if not isinstance(request._auth, UserPermissions):
# check only for not-secret auth
SessionAuthentication().enforce_csrf(request)
print(request.user)
if not request.user.is_superuser:
# todo: make this proper
raise PermissionDenied()
# todo: permissions
try:
with transaction.atomic():
version_data = json.loads(request.data["version"])
version = FirmwareVersion.objects.create(
project_name=version_data["project_name"],
version=version_data["version"],
idf_version=version_data["idf_version"],
uploader=request.user,
)
for variant, build_data in version_data["builds"].items():
bin_file = request.data[f"build_{variant}"]
if bin_file.size > 4*1024*1024:
raise ValueError # todo: better error
h = hashlib.sha256()
h.update(bin_file.open('rb').read())
sha256_bin_file = h.hexdigest()
if sha256_bin_file != build_data["sha256_hash"]:
raise ValueError
build = version.builds.create(
variant=variant,
chip=[chiptype.value for chiptype in ChipType
if chiptype.name.replace('_', '').lower() == build_data["chip"]][0],
sha256_hash=sha256_bin_file,
project_description=build_data["project_description"],
binary=bin_file,
)
for board in build_data["boards"]:
build.firmwarebuildboard_set.create(board=board)
except: # noqa
raise # todo: better error handling
return Response(version.serialize(), status=HTTP_201_CREATED)

View file

@ -1,5 +1,6 @@
# Generated by Django 4.2.1 on 2023-11-05 17:31
# Generated by Django 4.2.1 on 2023-11-05 18:07
import c3nav.mesh.models
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
@ -42,10 +43,16 @@ class Migration(migrations.Migration):
max_length=64, unique=True, verbose_name="SHA256 hash"
),
),
(
"project_description",
models.JSONField(verbose_name="project_description.json"),
),
(
"binary",
models.FileField(
null=True, upload_to="", verbose_name="firmware file"
null=True,
upload_to=c3nav.mesh.models.firmware_upload_path,
verbose_name="firmware file",
),
),
],

View file

@ -155,7 +155,7 @@ class FirmwareBuild(models.Model):
variant = models.CharField(_('variant name'), max_length=64)
chip = models.SmallIntegerField(_('chip'), db_index=True, choices=CHIPS)
sha256_hash = models.CharField(_('SHA256 hash'), unique=True, max_length=64)
project_description = models.JSONField
project_description = models.JSONField(verbose_name=_('project_description.json'))
binary = models.FileField(_('firmware file'), null=True, upload_to=firmware_upload_path)
class Meta:

View file

@ -317,6 +317,7 @@ USE_TZ = True
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.SessionAuthentication',
'c3nav.api.auth.APISecretAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.AllowAny',