diff options
author | Nikolay Zamotaev <nzamotaev@luxoft.com> | 2018-10-02 17:40:35 +0300 |
---|---|---|
committer | Nikolay Zamotaev <nzamotaev@luxoft.com> | 2018-10-18 11:12:59 +0000 |
commit | 967863e0a3755dd30478467c18ed71f64cef8e72 (patch) | |
tree | c96a145825e9253e61aff779d0ef7fb79ed58611 | |
parent | 48576772dce8bd135d276f47889c4bbc11cfcc06 (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.py | 1 | ||||
-rw-r--r-- | store/api.py | 60 | ||||
-rw-r--r-- | store/authdecorators.py | 135 | ||||
-rw-r--r-- | store/management/commands/store-upload-package.py | 39 | ||||
-rw-r--r-- | store/models.py | 38 |
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() |