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():
|
||||
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
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.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",
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue