From b8ca7286f2e2767a56e0b21f24bd3f25d2abdbbf Mon Sep 17 00:00:00 2001 From: William Colmenares Date: Thu, 9 May 2019 01:34:18 -0400 Subject: [PATCH 01/15] Add view to check if the vm belongs to a user --- hosting/urls.py | 3 ++- hosting/views.py | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/hosting/urls.py b/hosting/urls.py index a3579f06..1d0033ef 100644 --- a/hosting/urls.py +++ b/hosting/urls.py @@ -9,13 +9,14 @@ from .views import ( HostingPricingView, CreateVirtualMachinesView, HostingBillListView, HostingBillDetailView, SSHKeyDeleteView, SSHKeyCreateView, SSHKeyListView, SSHKeyChoiceView, DashboardView, SettingsView, ResendActivationEmailView, - InvoiceListView, InvoiceDetailView + InvoiceListView, InvoiceDetailView, CheckUserVM ) urlpatterns = [ url(r'index/?$', IndexView.as_view(), name='index'), url(r'django/?$', DjangoHostingView.as_view(), name='djangohosting'), + url(r'checkvm/?$', CheckUserVM.as_view(), name='chech_vm'), url(r'dashboard/?$', DashboardView.as_view(), name='dashboard'), url(r'nodejs/?$', NodeJSHostingView.as_view(), name='nodejshosting'), url(r'rails/?$', RailsHostingView.as_view(), name='railshosting'), diff --git a/hosting/views.py b/hosting/views.py index 92dd5aa8..4c0d8b41 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -26,6 +26,8 @@ from django.views.generic import ( View, CreateView, FormView, ListView, DetailView, DeleteView, TemplateView, UpdateView ) +from rest_framework.views import APIView +from rest_framework.response import Response from guardian.mixins import PermissionRequiredMixin from oca.pool import WrongIdError from stored_messages.api import mark_read @@ -1755,3 +1757,21 @@ def forbidden_view(request, exception=None, reason=''): 'again.') messages.add_message(request, messages.ERROR, err_msg) return HttpResponseRedirect(request.get_full_path()) + + +class CheckUserVM(APIView): + + def get(self, request): + try: + email = request.data['email'] + ip = request.data['ip'] + uservms = VMDetail.objects.filter(user__email=email) + if len(uservms) > 0: + for i in range(len(uservms)): + if uservms[i].ipv4 == ip or uservms[i].ipv6 == ip: + return Response('success', 200) + return Response('No VM found matching the ip address provided', 403) + else: + return Response('No VM found with the given email address', 403) + except KeyError: + return Response('Not enough data provided', 400) From 1faf46cc1b57a1e737d8ba694d3dff1e2ada3a36 Mon Sep 17 00:00:00 2001 From: William Colmenares Date: Sun, 12 May 2019 21:34:10 -0400 Subject: [PATCH 02/15] added validation to heck if the user is the one allowed to access --- hosting/views.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/hosting/views.py b/hosting/views.py index 4c0d8b41..f39e1b58 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -1,5 +1,7 @@ import logging import uuid +import os +import dotenv from datetime import datetime from time import sleep @@ -28,6 +30,7 @@ from django.views.generic import ( ) from rest_framework.views import APIView from rest_framework.response import Response +from rest_framework.renderers import JSONRenderer from guardian.mixins import PermissionRequiredMixin from oca.pool import WrongIdError from stored_messages.api import mark_read @@ -36,7 +39,7 @@ from stored_messages.settings import stored_messages_settings from datacenterlight.cms_models import DCLCalculatorPluginModel from datacenterlight.models import VMTemplate, VMPricing -from datacenterlight.utils import create_vm, get_cms_integration +from datacenterlight.utils import create_vm, get_cms_integration, check_otp, env from hosting.models import UserCardDetail from membership.models import CustomUser, StripeCustomer from opennebula_api.models import OpenNebulaManager @@ -68,9 +71,12 @@ from .models import ( logger = logging.getLogger(__name__) + CONNECTION_ERROR = "Your VMs cannot be displayed at the moment due to a \ backend connection error. please try again in a few \ minutes." + + decorators = [never_cache] @@ -1760,11 +1766,20 @@ def forbidden_view(request, exception=None, reason=''): class CheckUserVM(APIView): + renderer_classes = (JSONRenderer, ) def get(self, request): try: email = request.data['email'] ip = request.data['ip'] + user = request.data['user'] + realm = request.data['realm'] + token = request.data['token'] + if user != env('ACCOUNT_NAME'): + return Response("User not allowed", 403) + response = check_otp(user, realm, token) + if response != 200: + return Response('Invalid token', 403) uservms = VMDetail.objects.filter(user__email=email) if len(uservms) > 0: for i in range(len(uservms)): From fda5118c39029d27c369e4fc762b6736e3d7e1fb Mon Sep 17 00:00:00 2001 From: William Colmenares Date: Sun, 12 May 2019 21:34:54 -0400 Subject: [PATCH 03/15] Added otp verification --- datacenterlight/utils.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/datacenterlight/utils.py b/datacenterlight/utils.py index bbcb16ab..808a7643 100644 --- a/datacenterlight/utils.py +++ b/datacenterlight/utils.py @@ -1,4 +1,8 @@ import logging +import os +import pyotp +import requests +import dotenv from django.contrib.sites.models import Site from datacenterlight.tasks import create_vm_task @@ -11,6 +15,17 @@ from .models import VMPricing, VMTemplate logger = logging.getLogger(__name__) +PROJECT_DIR = os.path.abspath( + os.path.join(os.path.dirname(__file__)), +) + +# load .env file +dotenv.read_dotenv("{0}/.env".format(PROJECT_DIR)) + + +def env(env_name): + return os.environ.get(env_name) + def get_cms_integration(name): current_site = Site.objects.get_current() @@ -100,3 +115,22 @@ def clear_all_session_vars(request): 'generic_payment_details', 'product_id']: if session_var in request.session: del request.session[session_var] + + +def check_otp(name, realm, token): + data = { + "auth_name": env('AUTH_NAME'), + "auth_token": pyotp.TOTP(env('AUTH_SEED')).now(), + "auth_realm": env('AUTH_REALM'), + "name": name, + "realm": realm, + "token": token + } + response = requests.post( + "https://{OTP_SERVER}{OTP_VERIFY_ENDPOINT}".format( + OTP_SERVER=env('OTP_SERVER'), + OTP_VERIFY_ENDPOINT=env('OTP_VERIFY_ENDPOINT') + ), + data=data + ) + return response.status_code From 5e2e906f48d70c12244fda0557e4c1c1ce923df0 Mon Sep 17 00:00:00 2001 From: William Colmenares Date: Sun, 12 May 2019 21:35:28 -0400 Subject: [PATCH 04/15] include pyotp in requeriments --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index fe70299b..22da4c26 100644 --- a/requirements.txt +++ b/requirements.txt @@ -98,3 +98,4 @@ amqp==2.2.1 vine==1.1.4 cdist==4.7.0 git+https://github.com/ungleich/djangocms-multisite.git#egg=djangocms_multisite +pyotp From 69ec7d2b4627a37e555a29e087d6803c49f37050 Mon Sep 17 00:00:00 2001 From: William Colmenares Date: Mon, 13 May 2019 03:44:09 -0400 Subject: [PATCH 05/15] reuse of the env variable in the base settings --- datacenterlight/utils.py | 25 ++++++------------------- dynamicweb/settings/base.py | 8 ++++++++ hosting/views.py | 7 +++---- 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/datacenterlight/utils.py b/datacenterlight/utils.py index 808a7643..208d39f3 100644 --- a/datacenterlight/utils.py +++ b/datacenterlight/utils.py @@ -1,9 +1,8 @@ import logging -import os import pyotp import requests -import dotenv from django.contrib.sites.models import Site +from django.conf import settings from datacenterlight.tasks import create_vm_task from hosting.models import HostingOrder, HostingBill, OrderDetail @@ -15,18 +14,6 @@ from .models import VMPricing, VMTemplate logger = logging.getLogger(__name__) -PROJECT_DIR = os.path.abspath( - os.path.join(os.path.dirname(__file__)), -) - -# load .env file -dotenv.read_dotenv("{0}/.env".format(PROJECT_DIR)) - - -def env(env_name): - return os.environ.get(env_name) - - def get_cms_integration(name): current_site = Site.objects.get_current() try: @@ -119,17 +106,17 @@ def clear_all_session_vars(request): def check_otp(name, realm, token): data = { - "auth_name": env('AUTH_NAME'), - "auth_token": pyotp.TOTP(env('AUTH_SEED')).now(), - "auth_realm": env('AUTH_REALM'), + "auth_name": settings.AUTH_NAME, + "auth_token": pyotp.TOTP(settings.AUTH_SEED).now(), + "auth_realm": settings.AUTH_REALM, "name": name, "realm": realm, "token": token } response = requests.post( "https://{OTP_SERVER}{OTP_VERIFY_ENDPOINT}".format( - OTP_SERVER=env('OTP_SERVER'), - OTP_VERIFY_ENDPOINT=env('OTP_VERIFY_ENDPOINT') + OTP_SERVER=settings.OTP_SERVER, + OTP_VERIFY_ENDPOINT=settings.OTP_VERIFY_ENDPOINT ), data=data ) diff --git a/dynamicweb/settings/base.py b/dynamicweb/settings/base.py index b267c31d..27909813 100644 --- a/dynamicweb/settings/base.py +++ b/dynamicweb/settings/base.py @@ -721,6 +721,14 @@ X_FRAME_OPTIONS = ('SAMEORIGIN' if X_FRAME_OPTIONS_ALLOW_FROM_URI is None else DEBUG = bool_env('DEBUG') +ACCOUNT_NAME = env('ACCOUNT_NAME') +AUTH_NAME = env('AUTH_NAME') +AUTH_SEED = env('AUTH_SEED') +AUTH_REALM = env('AUTH_REALM') +OTP_SERVER = env('OTP_SERVER') +OTP_VERIFY_ENDPOINT = env('OTP_VERIFY_ENDPOINT') + + if DEBUG: from .local import * # flake8: noqa else: diff --git a/hosting/views.py b/hosting/views.py index f39e1b58..b0cee45c 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -1,7 +1,5 @@ import logging import uuid -import os -import dotenv from datetime import datetime from time import sleep @@ -39,7 +37,7 @@ from stored_messages.settings import stored_messages_settings from datacenterlight.cms_models import DCLCalculatorPluginModel from datacenterlight.models import VMTemplate, VMPricing -from datacenterlight.utils import create_vm, get_cms_integration, check_otp, env +from datacenterlight.utils import create_vm, get_cms_integration, check_otp from hosting.models import UserCardDetail from membership.models import CustomUser, StripeCustomer from opennebula_api.models import OpenNebulaManager @@ -1775,7 +1773,8 @@ class CheckUserVM(APIView): user = request.data['user'] realm = request.data['realm'] token = request.data['token'] - if user != env('ACCOUNT_NAME'): + print(settings.ACCOUNT_NAME) + if user != settings.ACCOUNT_NAME: return Response("User not allowed", 403) response = check_otp(user, realm, token) if response != 200: From 94d5c34152055be753acb38cefbd97cb0e051d56 Mon Sep 17 00:00:00 2001 From: PCoder Date: Mon, 13 May 2019 21:15:38 +0200 Subject: [PATCH 06/15] [hosting/bill] Skip creating MHB for invoices that have been imported already --- hosting/management/commands/fetch_stripe_bills.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/hosting/management/commands/fetch_stripe_bills.py b/hosting/management/commands/fetch_stripe_bills.py index df30535c..20f1cbe0 100644 --- a/hosting/management/commands/fetch_stripe_bills.py +++ b/hosting/management/commands/fetch_stripe_bills.py @@ -45,7 +45,12 @@ class Command(BaseCommand): num_invoice_created = 0 for invoice in all_invoices: invoice['customer'] = user.stripecustomer - num_invoice_created += 1 if MonthlyHostingBill.create(invoice) is not None else logger.error("Did not import invoice for %s" % str(invoice)) + try: + existing_mhb = MonthlyHostingBill.objects.get(invoice_id=invoice['invoice_id']) + logger.debug("Invoice %s exists already. Not importing." % invoice['invoice_id']) + except MonthlyHostingBill.DoesNotExist as dne: + logger.debug("Invoice id %s does not exist" % invoice['invoice_id']) + num_invoice_created += 1 if MonthlyHostingBill.create(invoice) is not None else logger.error("Did not import invoice for %s" % str(invoice)) self.stdout.write( self.style.SUCCESS("Number of invoices imported = %s" % num_invoice_created) ) From ce630573e027fdea9a6cad8c31bf6620e2b5bc89 Mon Sep 17 00:00:00 2001 From: William Colmenares Date: Thu, 16 May 2019 13:33:31 -0400 Subject: [PATCH 07/15] Remove print statement & correct code return --- hosting/views.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/hosting/views.py b/hosting/views.py index b0cee45c..88adaf22 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -1773,7 +1773,6 @@ class CheckUserVM(APIView): user = request.data['user'] realm = request.data['realm'] token = request.data['token'] - print(settings.ACCOUNT_NAME) if user != settings.ACCOUNT_NAME: return Response("User not allowed", 403) response = check_otp(user, realm, token) @@ -1784,8 +1783,8 @@ class CheckUserVM(APIView): for i in range(len(uservms)): if uservms[i].ipv4 == ip or uservms[i].ipv6 == ip: return Response('success', 200) - return Response('No VM found matching the ip address provided', 403) + return Response('No VM found matching the ip address provided', 404) else: - return Response('No VM found with the given email address', 403) + return Response('No VM found with the given email address', 404) except KeyError: return Response('Not enough data provided', 400) From a82ecbe4d50d7bd3e7fd0497b930ad4844f58309 Mon Sep 17 00:00:00 2001 From: William Colmenares Date: Thu, 16 May 2019 13:34:13 -0400 Subject: [PATCH 08/15] fix typho in check_vm --- hosting/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hosting/urls.py b/hosting/urls.py index 1d0033ef..2c8ff8ab 100644 --- a/hosting/urls.py +++ b/hosting/urls.py @@ -16,7 +16,7 @@ from .views import ( urlpatterns = [ url(r'index/?$', IndexView.as_view(), name='index'), url(r'django/?$', DjangoHostingView.as_view(), name='djangohosting'), - url(r'checkvm/?$', CheckUserVM.as_view(), name='chech_vm'), + url(r'checkvm/?$', CheckUserVM.as_view(), name='check_vm'), url(r'dashboard/?$', DashboardView.as_view(), name='dashboard'), url(r'nodejs/?$', NodeJSHostingView.as_view(), name='nodejshosting'), url(r'rails/?$', RailsHostingView.as_view(), name='railshosting'), From 0cada8668a135253a5220b8b73c4ccb1f0dcda43 Mon Sep 17 00:00:00 2001 From: PCoder Date: Thu, 16 May 2019 22:10:41 +0200 Subject: [PATCH 09/15] Update Changelog for 2.5.10 --- Changelog | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Changelog b/Changelog index ec77e55b..747d4028 100644 --- a/Changelog +++ b/Changelog @@ -1,4 +1,5 @@ -Next: +2.5.10: 2019-05-16 + * #6672: [api] REST endpoint for ungleich-cli to verify if a VM belongs to a user (MR!705) * #6670: [hosting/save_ssh_key] Upgrade cdist version to 5.0.1 to manage keys on Alpine linux 2.5.9: 2019-05-09 * #6669: [hosting] Fix opennebula vm query takes long (MR!703) From 5ad871f1248027fba718ff0e5fda0a3dcd247751 Mon Sep 17 00:00:00 2001 From: William Colmenares Date: Thu, 16 May 2019 16:35:44 -0400 Subject: [PATCH 10/15] updated for read vm realm --- dynamicweb/settings/base.py | 2 +- hosting/views.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dynamicweb/settings/base.py b/dynamicweb/settings/base.py index 27909813..1051c4ab 100644 --- a/dynamicweb/settings/base.py +++ b/dynamicweb/settings/base.py @@ -721,7 +721,7 @@ X_FRAME_OPTIONS = ('SAMEORIGIN' if X_FRAME_OPTIONS_ALLOW_FROM_URI is None else DEBUG = bool_env('DEBUG') -ACCOUNT_NAME = env('ACCOUNT_NAME') +READ_VM_REALM = env('READ_VM_REALM') AUTH_NAME = env('AUTH_NAME') AUTH_SEED = env('AUTH_SEED') AUTH_REALM = env('AUTH_REALM') diff --git a/hosting/views.py b/hosting/views.py index 88adaf22..cd6aa4f3 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -1773,7 +1773,7 @@ class CheckUserVM(APIView): user = request.data['user'] realm = request.data['realm'] token = request.data['token'] - if user != settings.ACCOUNT_NAME: + if realm != settings.READ_VM_REALM: return Response("User not allowed", 403) response = check_otp(user, realm, token) if response != 200: From 63a78a537e1300e393f6fcf54aae799210618264 Mon Sep 17 00:00:00 2001 From: PCoder Date: Sat, 8 Jun 2019 04:25:55 +0200 Subject: [PATCH 11/15] Use infoextended for fallback case also --- opennebula_api/models.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/opennebula_api/models.py b/opennebula_api/models.py index a333aa23..a951349e 100644 --- a/opennebula_api/models.py +++ b/opennebula_api/models.py @@ -212,7 +212,13 @@ class OpenNebulaManager(): 'Could not connect via client, using oneadmin instead') try: vm_pool = oca.VirtualMachinePool(self.oneadmin_client) - vm_pool.info(filter=-2) + if infoextended: + vm_pool.infoextended( + filter=-1, # User's resources and any of his groups + vm_state=-1 # Look for VMs in any state, except DONE + ) + else: + vm_pool.info(filter=-2) return vm_pool except: raise ConnectionRefusedError From 496178f44cbfbfc585cc8c0ddd918c9b2ff98e08 Mon Sep 17 00:00:00 2001 From: PCoder Date: Sat, 8 Jun 2019 04:40:16 +0200 Subject: [PATCH 12/15] Check if VM belongs to user against opennebula backend --- hosting/views.py | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/hosting/views.py b/hosting/views.py index cd6aa4f3..11a4b8bc 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -26,11 +26,11 @@ from django.views.generic import ( View, CreateView, FormView, ListView, DetailView, DeleteView, TemplateView, UpdateView ) -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.renderers import JSONRenderer from guardian.mixins import PermissionRequiredMixin from oca.pool import WrongIdError +from rest_framework.renderers import JSONRenderer +from rest_framework.response import Response +from rest_framework.views import APIView from stored_messages.api import mark_read from stored_messages.models import Message from stored_messages.settings import stored_messages_settings @@ -1778,13 +1778,23 @@ class CheckUserVM(APIView): response = check_otp(user, realm, token) if response != 200: return Response('Invalid token', 403) - uservms = VMDetail.objects.filter(user__email=email) - if len(uservms) > 0: - for i in range(len(uservms)): - if uservms[i].ipv4 == ip or uservms[i].ipv6 == ip: - return Response('success', 200) - return Response('No VM found matching the ip address provided', 404) - else: - return Response('No VM found with the given email address', 404) + manager = OpenNebulaManager() + # not the best way to lookup vms by ip + # TODO: make this optimal + vms = manager.get_vms() + users_vms = [VirtualMachineSerializer(vm).data for vm in vms + if vm.uname == email] + if len(users_vms) == 0: + return Response('No VM found with the given email address', + 404) + for vm in users_vms: + for nic in vm.template.nics: + if hasattr(nic, 'ip6_global'): + if nic.ip6_global == ip: + return Response('success', 200) + elif hasattr(nic, 'ip'): + if nic.ip == ip: + return Response('success', 200) + return Response('No VM found matching the ip address provided', 404) except KeyError: return Response('Not enough data provided', 400) From 1ebfc8b2dcf392958e14cde0075946ad231a889d Mon Sep 17 00:00:00 2001 From: PCoder Date: Mon, 10 Jun 2019 14:51:40 +0200 Subject: [PATCH 13/15] Don't use VirtualMachineSerializer for obtaining users_vms --- hosting/views.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/hosting/views.py b/hosting/views.py index 11a4b8bc..4c6ea04d 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -1782,8 +1782,7 @@ class CheckUserVM(APIView): # not the best way to lookup vms by ip # TODO: make this optimal vms = manager.get_vms() - users_vms = [VirtualMachineSerializer(vm).data for vm in vms - if vm.uname == email] + users_vms = [vm for vm in vms if vm.uname == email] if len(users_vms) == 0: return Response('No VM found with the given email address', 404) From e5f317281f869c9cc030db95d27cddbab389380d Mon Sep 17 00:00:00 2001 From: William Colmenares Date: Mon, 10 Jun 2019 18:25:18 -0400 Subject: [PATCH 14/15] fix translation Learn more -> Lerne mehr --- digitalglarus/locale/de/LC_MESSAGES/django.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/digitalglarus/locale/de/LC_MESSAGES/django.po b/digitalglarus/locale/de/LC_MESSAGES/django.po index 2d0129c4..ec96f5dc 100644 --- a/digitalglarus/locale/de/LC_MESSAGES/django.po +++ b/digitalglarus/locale/de/LC_MESSAGES/django.po @@ -342,7 +342,7 @@ msgstr "" "dieser Website erklärst Du Dich damit einverstanden, diese zu nutzen." msgid "Learn more" -msgstr "Learn mehr" +msgstr "Lerne mehr" msgid "OK" msgstr "" From 151983ff592d03145fc9d6870a5862079c301136 Mon Sep 17 00:00:00 2001 From: PCoder Date: Tue, 11 Jun 2019 01:09:45 +0200 Subject: [PATCH 15/15] Update Changelog for 2.5.11 --- Changelog | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Changelog b/Changelog index 747d4028..cfefdd01 100644 --- a/Changelog +++ b/Changelog @@ -1,3 +1,6 @@ +2.5.11: 2019-06-11 + * #6672: [api] Check VM belongs to user in the infrastructure directly (MR!707) + * #bugfix: DE translation fix "Learn mehr" -> "Lerne mehr" (MR!708) 2.5.10: 2019-05-16 * #6672: [api] REST endpoint for ungleich-cli to verify if a VM belongs to a user (MR!705) * #6670: [hosting/save_ssh_key] Upgrade cdist version to 5.0.1 to manage keys on Alpine linux