diff --git a/src/c3nav/api/auth.py b/src/c3nav/api/auth.py new file mode 100644 index 00000000..c7ea5528 --- /dev/null +++ b/src/c3nav/api/auth.py @@ -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) diff --git a/src/c3nav/control/views/users.py b/src/c3nav/control/views/users.py index a68e99f3..f5e68b68 100644 --- a/src/c3nav/control/views/users.py +++ b/src/c3nav/control/views/users.py @@ -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() diff --git a/src/c3nav/mesh/api.py b/src/c3nav/mesh/api.py new file mode 100644 index 00000000..1e3711db --- /dev/null +++ b/src/c3nav/mesh/api.py @@ -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) diff --git a/src/c3nav/mesh/migrations/0008_firmwarebuild_firmwarebuildboard_firmwareversion_and_more.py b/src/c3nav/mesh/migrations/0008_firmwarebuild_firmwarebuildboard_firmwareversion_and_more.py index a8c4d10d..8b52c15f 100644 --- a/src/c3nav/mesh/migrations/0008_firmwarebuild_firmwarebuildboard_firmwareversion_and_more.py +++ b/src/c3nav/mesh/migrations/0008_firmwarebuild_firmwarebuildboard_firmwareversion_and_more.py @@ -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", ), ), ], diff --git a/src/c3nav/mesh/models.py b/src/c3nav/mesh/models.py index 56b3791c..bec34bc6 100644 --- a/src/c3nav/mesh/models.py +++ b/src/c3nav/mesh/models.py @@ -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: diff --git a/src/c3nav/settings.py b/src/c3nav/settings.py index c66f5473..28946267 100644 --- a/src/c3nav/settings.py +++ b/src/c3nav/settings.py @@ -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',