add api_secret auth and (mostly) finalize firmware endpoint
This commit is contained in:
parent
9e9e41fb3f
commit
aa2df8d3c5
6 changed files with 123 additions and 4 deletions
20
src/c3nav/api/auth.py
Normal file
20
src/c3nav/api/auth.py
Normal 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)
|
|
@ -76,7 +76,7 @@ def user_detail(request, user): # todo: make class based view
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
if api_secret_action in ('generate', 'regenerate'):
|
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.api_secret = api_secret
|
||||||
permissions.save()
|
permissions.save()
|
||||||
|
|
||||||
|
|
91
src/c3nav/mesh/api.py
Normal file
91
src/c3nav/mesh/api.py
Normal 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)
|
|
@ -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.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
@ -42,10 +43,16 @@ class Migration(migrations.Migration):
|
||||||
max_length=64, unique=True, verbose_name="SHA256 hash"
|
max_length=64, unique=True, verbose_name="SHA256 hash"
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"project_description",
|
||||||
|
models.JSONField(verbose_name="project_description.json"),
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"binary",
|
"binary",
|
||||||
models.FileField(
|
models.FileField(
|
||||||
null=True, upload_to="", verbose_name="firmware file"
|
null=True,
|
||||||
|
upload_to=c3nav.mesh.models.firmware_upload_path,
|
||||||
|
verbose_name="firmware file",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -155,7 +155,7 @@ class FirmwareBuild(models.Model):
|
||||||
variant = models.CharField(_('variant name'), max_length=64)
|
variant = models.CharField(_('variant name'), max_length=64)
|
||||||
chip = models.SmallIntegerField(_('chip'), db_index=True, choices=CHIPS)
|
chip = models.SmallIntegerField(_('chip'), db_index=True, choices=CHIPS)
|
||||||
sha256_hash = models.CharField(_('SHA256 hash'), unique=True, max_length=64)
|
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)
|
binary = models.FileField(_('firmware file'), null=True, upload_to=firmware_upload_path)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -317,6 +317,7 @@ USE_TZ = True
|
||||||
REST_FRAMEWORK = {
|
REST_FRAMEWORK = {
|
||||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||||
'rest_framework.authentication.SessionAuthentication',
|
'rest_framework.authentication.SessionAuthentication',
|
||||||
|
'c3nav.api.auth.APISecretAuthentication',
|
||||||
),
|
),
|
||||||
'DEFAULT_PERMISSION_CLASSES': (
|
'DEFAULT_PERMISSION_CLASSES': (
|
||||||
'rest_framework.permissions.AllowAny',
|
'rest_framework.permissions.AllowAny',
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue