summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNikolay Zamotaev <nzamotaev@luxoft.com>2018-10-02 17:40:35 +0300
committerNikolay Zamotaev <nzamotaev@luxoft.com>2018-10-18 11:12:59 +0000
commit967863e0a3755dd30478467c18ed71f64cef8e72 (patch)
treec96a145825e9253e61aff779d0ef7fb79ed58611
parent48576772dce8bd135d276f47889c4bbc11cfcc06 (diff)
Remote package upload API implementation
Any user with permission to enter admin panel can upload packages to the deployment server. This will be used by a (not yet implemented) tool in qt application manager for automatic package upload from qtcreator. Fixes: AUTOSUITE-623 Change-Id: I5aba9d16480e2161e5e633359070004f66f2b897 Reviewed-by: Dominik Holland <dominik.holland@pelagicore.com>
-rw-r--r--appstore/urls.py1
-rw-r--r--store/api.py60
-rw-r--r--store/authdecorators.py135
-rw-r--r--store/management/commands/store-upload-package.py39
-rw-r--r--store/models.py38
5 files changed, 233 insertions, 40 deletions
diff --git a/appstore/urls.py b/appstore/urls.py
index 081acf9..327c28b 100644
--- a/appstore/urls.py
+++ b/appstore/urls.py
@@ -46,6 +46,7 @@ base_urlpatterns = patterns('',
url(r'^app/download/(.*)$', 'store.api.appDownload'),
url(r'^category/list$', 'store.api.categoryList'),
url(r'^category/icon$', 'store.api.categoryIcon'),
+ url(r'^upload$', 'store.api.upload'),
)
diff --git a/store/api.py b/store/api.py
index 9734f6e..f2d911e 100644
--- a/store/api.py
+++ b/store/api.py
@@ -30,19 +30,19 @@
#############################################################################
import os
-import tempfile
-import datetime
import shutil
-import json
from django.conf import settings
from django.db.models import Q, Count
from django.http import HttpResponse, HttpResponseForbidden, Http404, JsonResponse
from django.contrib import auth
from django.template import Context, loader
+from django.views.decorators.csrf import csrf_exempt
+from authdecorators import logged_in_or_basicauth, is_staff_member
-from models import App, Category, Vendor
-from utilities import parsePackageMetadata, packagePath, iconPath, downloadPath, addSignatureToPackage, validateTag
+from models import App, Category, Vendor, savePackageFile
+from utilities import parsePackageMetadata, parseAndValidatePackageMetadata, addSignatureToPackage
+from utilities import packagePath, iconPath, downloadPath, validateTag
def hello(request):
@@ -105,6 +105,56 @@ def logout(request):
return JsonResponse({'status': status})
+@csrf_exempt
+@logged_in_or_basicauth()
+@is_staff_member()
+def upload(request):
+ status = 'ok'
+ try:
+ try:
+ description = request.REQUEST["description"]
+ except:
+ raise Exception('no description')
+ try:
+ shortdescription = request.REQUEST["short-description"]
+ except:
+ raise Exception('no short description')
+ try:
+ category_name = request.REQUEST["category"]
+ except:
+ raise Exception('no category')
+ try:
+ vendor_name = request.REQUEST["vendor"]
+ except:
+ raise Exception('no vendor')
+
+ if request.method == 'POST' and request.FILES['package']:
+ myfile = request.FILES['package']
+ category = Category.objects.all().filter(name__exact=category_name)
+ vendor = Vendor.objects.all().filter(name__exact=vendor_name)
+ if len(category) == 0:
+ raise Exception('Non-existing category')
+ if len(vendor) == 0:
+ raise Exception('Non-existing vendor')
+
+ try:
+ pkgdata = parseAndValidatePackageMetadata(myfile)
+ except:
+ raise Exception('Package validation failed')
+
+ myfile.seek(0)
+ try:
+ savePackageFile(pkgdata, myfile, category[0], vendor[0], description, shortdescription)
+ except Exception as error:
+ raise Exception(error)
+ else:
+ raise Exception('no package to upload')
+
+ except Exception as error:
+ status = str(error)
+ return JsonResponse({'status': status})
+
+
def appList(request):
apps = App.objects.all()
if 'filter' in request.REQUEST:
diff --git a/store/authdecorators.py b/store/authdecorators.py
new file mode 100644
index 0000000..fbbc01d
--- /dev/null
+++ b/store/authdecorators.py
@@ -0,0 +1,135 @@
+#############################################################################
+##
+## Copyright (C) 2018 Luxoft
+## Contact: https://www.qt.io/licensing/
+##
+## This file is part of the Neptune Deployment Server
+##
+## $QT_BEGIN_LICENSE:GPL-QTAS$
+## Commercial License Usage
+## Licensees holding valid commercial Qt Automotive Suite licenses may use
+## this file in accordance with the commercial license agreement provided
+## with the Software or, alternatively, in accordance with the terms
+## contained in a written agreement between you and The Qt Company. For
+## licensing terms and conditions see https://www.qt.io/terms-conditions.
+## For further information use the contact form at https://www.qt.io/contact-us.
+##
+## GNU General Public License Usage
+## Alternatively, this file may be used under the terms of the GNU
+## General Public License version 3 or (at your option) any later version
+## approved by the KDE Free Qt Foundation. The licenses are as published by
+## the Free Software Foundation and appearing in the file LICENSE.GPL3
+## included in the packaging of this file. Please review the following
+## information to ensure the GNU General Public License requirements will
+## be met: https://www.gnu.org/licenses/gpl-3.0.html.
+##
+## $QT_END_LICENSE$
+##
+## SPDX-License-Identifier: GPL-3.0
+##
+#############################################################################
+
+# Code taken from: https://www.djangosnippets.org/snippets/243/
+# Reuse and licensing is permitted by TOS: https://www.djangosnippets.org/about/tos/
+
+import base64
+
+from django.http import HttpResponse
+from django.contrib.auth import authenticate, login
+
+#############################################################################
+
+def view_or_basicauth(view, request, test_func, realm="", *args, **kwargs):
+ """
+ This is a helper function used by both 'logged_in_or_basicauth' and
+ 'has_perm_or_basicauth' that does the service of determining if they
+ are already logged in or if they have provided proper http-authorization
+ and returning the view if all goes well, otherwise responding with a 401.
+ """
+ if test_func(request.user):
+ # Already logged in, just return the view.
+ #
+ return view(request, *args, **kwargs)
+
+ # They are not logged in. See if they provided login credentials
+ #
+ if 'HTTP_AUTHORIZATION' in request.META:
+ auth = request.META['HTTP_AUTHORIZATION'].split()
+ if len(auth) == 2:
+ # NOTE: We are only support basic authentication for now.
+ #
+ if auth[0].lower() == "basic":
+ uname, passwd = base64.b64decode(auth[1]).split(':')
+ user = authenticate(username=uname, password=passwd)
+ if user is not None:
+ if user.is_active:
+ login(request, user)
+ request.user = user
+ if test_func(request.user):
+ return view(request, *args, **kwargs)
+
+ # Either they did not provide an authorization header or
+ # something in the authorization attempt failed. Send a 401
+ # back to them to ask them to authenticate.
+ #
+ response = HttpResponse()
+ response.status_code = 401
+ response['WWW-Authenticate'] = 'Basic realm="%s"' % realm
+ return response
+
+
+#############################################################################
+
+def logged_in_or_basicauth(realm=""):
+ """
+ A simple decorator that requires a user to be logged in and in staff group.
+ If they are not logged in the request is examined for a 'authorization' header.
+
+ If the header is present it is tested for basic authentication and
+ the user is logged in with the provided credentials.
+
+ If the header is not present a http 401 is sent back to the
+ requester to provide credentials.
+
+ The purpose of this is that in several django projects I have needed
+ several specific views that need to support basic authentication, yet the
+ web site as a whole used django's provided authentication.
+
+ The uses for this are for urls that are access programmatically such as
+ by rss feed readers, yet the view requires a user to be logged in. Many rss
+ readers support supplying the authentication credentials via http basic
+ auth (and they do NOT support a redirect to a form where they post a
+ username/password.)
+
+ Use is simple:
+
+ @logged_in_or_basicauth
+ def your_view:
+ ...
+
+ You can provide the name of the realm to ask for authentication within.
+ """
+
+ def view_decorator(func):
+ def wrapper(request, *args, **kwargs):
+ return view_or_basicauth(func, request,
+ lambda u: u.is_authenticated(),
+ realm, *args, **kwargs)
+
+ return wrapper
+
+ return view_decorator
+
+def is_staff_member():
+ def view_decorator(func):
+ def wrapper(request, *args, **kwargs):
+ if request.user.is_staff:
+ return func(request, *args, **kwargs)
+ else:
+ response = HttpResponse()
+ response.status_code = 403
+ return response
+
+ return wrapper
+ return view_decorator
+
diff --git a/store/management/commands/store-upload-package.py b/store/management/commands/store-upload-package.py
index 1468d27..81f96fa 100644
--- a/store/management/commands/store-upload-package.py
+++ b/store/management/commands/store-upload-package.py
@@ -33,8 +33,8 @@ import os
from django.core.management.base import BaseCommand, CommandError
from django.core.files.base import ContentFile
-from store.models import App, Category, Vendor
-from store.utilities import parseAndValidatePackageMetadata, packagePath, makeTagList, writeTempIcon
+from store.models import App, Category, Vendor, savePackageFile
+from store.utilities import parseAndValidatePackageMetadata
from optparse import make_option
@@ -83,38 +83,9 @@ class Command(BaseCommand):
return 0
packagefile.seek(0)
- appId = pkgdata['info']['id']
- name = pkgdata['storeName']
- architecture = pkgdata['architecture']
description = options['description']
- tags = makeTagList(pkgdata)
-
- success, error = writeTempIcon(appId, architecture, pkgdata['icon'])
- if not success:
- raise CommandError(error)
-
- exists = False
- app = None
try:
- app = App.objects.get(appid__exact=appId, architecture__exact= architecture)
- exists = True
- except App.DoesNotExist:
- pass
+ savePackageFile(pkgdata, ContentFile(packagefile.read()), category[0], vendor[0], description, description)
+ except Exception as error:
+ raise CommandError(error)
- if exists:
- app.appid = appId
- app.category = category[0]
- app.vendor = vendor[0]
- app.name = name
- app.tags = tags
- app.description = app.briefDescription = description
- app.architecture = architecture
- app.file.save(packagePath(appId, architecture), ContentFile(packagefile.read()))
- app.save()
- else:
- app, created = App.objects.get_or_create(name=name, tags=tags, vendor=vendor[0],
- category=category[0], appid=appId,
- briefDescription=description, description=description,
- architecture=architecture)
- app.file.save(packagePath(appId, architecture), ContentFile(packagefile.read()))
- app.save()
diff --git a/store/models.py b/store/models.py
index 802d141..63b1d89 100644
--- a/store/models.py
+++ b/store/models.py
@@ -36,7 +36,7 @@ from django.conf import settings
from django.contrib.auth.models import User
from django.core.files.storage import FileSystemStorage
-from utilities import packagePath
+from utilities import packagePath, writeTempIcon, makeTagList
class Category(models.Model):
@@ -142,3 +142,39 @@ class App(models.Model):
pass
super(App, self).save(*args, **kwargs)
+
+def savePackageFile(pkgdata, pkgfile, category, vendor, description, shortdescription):
+ appId = pkgdata['info']['id']
+ name = pkgdata['storeName']
+ architecture = pkgdata['architecture']
+ tags = makeTagList(pkgdata)
+ success, error = writeTempIcon(appId, architecture, pkgdata['icon'])
+ if not success:
+ raise Exception(error)
+
+ exists = False
+ app = None
+ try:
+ app = App.objects.get(appid__exact=appId, architecture__exact=architecture)
+ exists = True
+ except App.DoesNotExist:
+ pass
+
+ if exists:
+ app.appid = appId
+ app.category = category
+ app.vendor = vendor
+ app.name = name
+ app.tags = tags
+ app.description = description
+ app.briefDescription = shortdescription
+ app.architecture = architecture
+ app.file.save(packagePath(appId, architecture), pkgfile)
+ app.save()
+ else:
+ app, created = App.objects.get_or_create(name=name, tags=tags, vendor=vendor,
+ category=category, appid=appId,
+ briefDescription=shortdescription, description=description,
+ architecture=architecture)
+ app.file.save(packagePath(appId, architecture), pkgfile)
+ app.save()