diff --git a/.gitignore b/.gitignore index 7bbc71c092..b823e313b4 100644 --- a/.gitignore +++ b/.gitignore @@ -99,3 +99,5 @@ ENV/ # mypy .mypy_cache/ + +.vagrant diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..b72f88a6e0 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,21 @@ +dist: xenial + +services: + - postgresql +addons: + postgresql: "9.4" +before_script: + - psql -U postgres -c "create user decide password 'decide'" + - psql -U postgres -c "create database test_decide owner decide" + - psql -U postgres -c "ALTER USER decide CREATEDB" +language: python +python: + - "3.6" +install: + - pip install -r requirements.txt + - pip install codacy-coverage +script: + - cd decide + - coverage run --branch --source=. ./manage.py test --keepdb --with-xunit + - coverage xml + - python-codacy-coverage -r coverage.xml diff --git a/README.md b/README.md index 9ca86f1d2f..83d0a57e27 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![Build Status](https://travis-ci.com/wadobo/decide.svg?branch=master)](https://travis-ci.com/wadobo/decide) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/94a85eaa0e974c71af6899ea3b0d27e0)](https://www.codacy.com/app/Wadobo/decide?utm_source=github.com&utm_medium=referral&utm_content=wadobo/decide&utm_campaign=Badge_Grade) [![Codacy Badge](https://api.codacy.com/project/badge/Coverage/94a85eaa0e974c71af6899ea3b0d27e0)](https://www.codacy.com/app/Wadobo/decide?utm_source=github.com&utm_medium=referral&utm_content=wadobo/decide&utm_campaign=Badge_Coverage) + Plataforma voto electrónico educativa ===================================== @@ -19,6 +21,33 @@ entre ellos. Para conseguir esto, los subsistemas se conectarán entre si median Este proyecto Django estará dividido en apps (subsistemas y proyecto base), donde cualquier app podrá ser reemplazada individualmente. +Gateway +------- + +Para ofrecer un punto de entrada conocido para todos los subsistemas +existe el llamado **gateway** que no es más que una ruta disponible +que redirigirá todas las peticiones al subsistema correspondiente, de +tal forma que cualquier cliente que use la API no tiene por qué saber +en qué servidor está desplegado cada subsistema. + +La ruta se compone de: + + http://DOMINIO/gateway/SUBSISTEMA/RUTA/EN/EL/SUBSISTEMA + +Por ejemplo para acceder al subsistema de autenticación y hacer la petición +al endpoint de /authentication/login/ deberíamos hacer la petición a la +siguiente ruta: + + http://DOMINIO/gateway/authentication/login/ + +Otro ejemplo sería para obtener una votación por id: + + http://DOMINIO/gateway/voting/?id=1 + +A nivel interno, el módulo `mods` ofrece esta funcionalidad, pero el +gateway es útil para hacer uso desde peticiones de cliente, por ejemplo +en el javascript de la cabina de votación o la visualización de resultados, +y también para módulos externos que no sean aplicaciones django. Configurar y ejecutar el proyecto --------------------------------- @@ -94,3 +123,170 @@ Lanzar tests: Lanzar una consola SQL: $ docker exec -ti decide_db ash -c "su - postgres -c 'psql postgres'" + +Ejecutar con vagrant + ansible +------------------------------ + +Existe una configuración de vagrant que crea una máquina virtual con todo +lo necesario instalado y listo para funcionar. La configuración está en +vagrant/Vagrantfile y por defecto utiliza Virtualbox, por lo que para +que esto funcione debes tener instalado en tu sistema vagrant y Virtualbox. + +Crear la máquina virtual con vagrant: + + $ cd vagrant + $ vagrant up + +Una vez creada podremos acceder a la web, con el usuario admin/admin: + +http://localhost:8080/admin + +Acceder por ssh a la máquina: + + $ vagrant ssh + +Esto nos dará una consola con el usuario vagrant, que tiene permisos de +sudo, por lo que podremos acceder al usuario administrador con: + + $ sudo su + +Parar la máquina virtual: + + $ vagrant stop + +Una vez parada la máquina podemos volver a lanzarla con `vagrant up`. + +Eliminar la máquina virtual: + + $ vagrant destroy + +Ansible +------- + +El provisionamiento de la aplicación con vagrant está hecho con Ansible, +algo que nos permite utilizarlo de forma independiente para provisionar +una instalación de Decide en uno o varios servidores remotos con un +simple comando. + + $ cd vagrant + $ ansible-playbook -i inventory playbook.yml + +Para que esto funcione debes definir un fichero [inventory](https://docs.ansible.com/ansible/latest/user_guide/intro_inventory.html) +con los servidores destino. + +Los scripts de ansible están divididos en varios ficheros .yml donde +se definen las diferentes tareas, por lo que es posible lanzar partes +independientes: + + * packages.yml, dependencias del sistema + * user.yml, creación de usuario decide + * python.yml, git clone del repositorio e instalación de dependencias python en virtualenv + * files.yml, ficheros de configuración, systemd, nginx y local\_settings.py + * database.yml, creación de usuario y base de datos postgres + * django.yml, comandos django básicos y creación de usuario admin + * services.yml, reinicio de servicios, decide, nginx y postgres + +Por ejemplo este comando sólo reinicia los servicios en el servidor: + + $ ansible-playbook -i inventory -t services + +El provisionamiento de ansible está diseñado para funcionar con **ubuntu/bionic64**, +para funcionar con otras distribuciones es posible que haga falta modificar +el fichero packages.yml. + +Versionado +---------- + +El versionado de API está hecho utilizando Django Rest Framework, y la forma +elegida para este versionado es mediante [parámetros de búsqueda](https://www.django-rest-framework.org/api-guide/versioning/#queryparameterversioning), +podemos cambiarlo a parámetros en la URL o en el nombre del HOST, hay diferentes +tipos de versionado disponibles en Django Rest Framework, podemos verlos +[aqui](https://www.django-rest-framework.org/api-guide/versioning/#versioning). + +Nosotros hemos escogido el de por parámetros por ser el más sencillo, y hemos +creado un ejemplo para que veamos su uso, podemos verlo en voting/views.py + +Si nosotros queremos que la salida que nos da la llamada a la API /voting/, sea +diferente en la versión 2, solo tenemos que comprobar en la versión nos está +llegando, y hacer lo que queramos, por ejemplo: + + +``` + def get(self, request, *args, **kwargs): + version = request.version # Con request.version obtenemos la versión + if version not in settings.ALLOWED_VERSIONS: # Versiones permitidas + version = settings.DEFAULT_VERSION # Si no existe: versión por defecto + # En el caso de usar la versión 2, usamos un serializador diferente + if version == 'v2': + self.serializer_class = SimpleVotingSerializer + return super().get(request, *args, **kwargs) +``` + +Para llamar a las diferentes versiones, haremos lo siguiente: + +* /voting/?version=v1 +* /voting/?version=v2 + + +Test de estrés con Locust +------------------------- + +Antes de empezar, comentaré para que sirven las pruebas de estrés. A veces necesitamos soportar que +nuestra aplicación ofrezca una cantidad de peticiones por segundo, porque habrá mucha gente entrando +a la misma vez, y ante este estrés, tenemos que comprobar como se comporta nuestra aplicación. + +No es lo mismo que cuando la estresemos nos de un error 500 a que nos devuelva la petición de otro +usuario. Con estos test conseguiremos comprobar cual es ese comportamiento, y quizás mejorar la +velocidad de las peticiones para permitir más peticiones por segundo. + +Para ejecutar los test de estrés utilizando locust, necesitaremos tener instalado locust: + + $ pip install locust + +Una vez instalado, necesitaremos tener un fichero locustfile.py donde tengamos la configuración de +lo que vamos a ejecutar. En nuestro caso, tenemos hecho dos ejemplos: + +1. Visualizer: entra en el visualizador de una votación para ver cuantas peticiones puede aguantar. + + Para ejecutar el test de Visualizer, tenemos que tener en cuenta que entra en la votación 1, por lo + que necesitaremos tenerla creada para que funcione correctamente, una vez hecho esto, podemos + comenzar a probar con el siguiente comando (dentro de la carpeta loadtest): + + $ locust Visualizer + + Esto abrirá un servidor que podremos ver en el navegador, el mismo comando nos dirá el puerto. + Cuando se abra, nos preguntará cuantos usuarios queremos que hagan peticiones a la vez, y como + queremos que vaya creciendo hasta llegar a ese número. Por ejemplo, si ponemos 100 y 5, estaremos + creando 5 nuevos usuarios cada segundo hasta llegar a 100. + +2. Voters: utilizaremos usuarios previamente creados, y haremos una secuencia de peticiones: login, +getuser y store. Sería lo que realizaría un usuario cuando va a votar, por lo que con este ejemplo +estaremos comprobando cuantas votaciones podemos hacer. + + + Para ejecutar el test de Voter, necesitaremos realizar varios preparos. Necesitaremos la votación 1 + abierta, y necesitaremos crear una serie de usuarios en el censo de esta votación, para que cuando + hagamos el test, estos usuario puedan autenticarse y votar correctamente. Para facilitar esta + tarea, hemos creado el script de python gen_census.py, en el cual creamos los usuarios que + tenemos dentro del fichero voters.json y los añadimos al censo utilizando la librería requests. + Para que este script funcione, necesitaremos tener instalado request: + + $ pip install requests + + Una vez instalado, ejecutamos el script: + + $ python gen_census.py + + Tras esto, ya podremos comenzar el test de estrés de votantes: + + $ locust Voters + +Importante mirar bien el fichero locustfile.py, donde existen algunas configuraciones que podremos +cambiar, dependiendo del HOST donde queramos hacer las pruebas y del id de la votación. + +A tener en cuenta: + +* En un servidor local, con un postgres que por defecto nos viene limitado a 100 usuarios + concurrentes, cuando pongamos más de 100, lo normal es que empiecen a fallar muchas peticiones. +* Si hacemos las pruebas en local, donde tenemos activado el modo debug de Django, lo normal es que + las peticiones tarden algo más y consigamos menos RPS (Peticiones por segundo). diff --git a/decide/authentication/tests.py b/decide/authentication/tests.py index 69178f914d..dfd09df191 100644 --- a/decide/authentication/tests.py +++ b/decide/authentication/tests.py @@ -17,6 +17,11 @@ def setUp(self): u.set_password('123') u.save() + u2 = User(username='admin') + u2.set_password('admin') + u2.is_superuser = True + u2.save() + def tearDown(self): self.client = None @@ -79,3 +84,47 @@ def test_logout(self): self.assertEqual(response.status_code, 200) self.assertEqual(Token.objects.filter(user__username='voter1').count(), 0) + + def test_register_bad_permissions(self): + data = {'username': 'voter1', 'password': '123'} + response = self.client.post('/authentication/login/', data, format='json') + self.assertEqual(response.status_code, 200) + token = response.json() + + token.update({'username': 'user1'}) + response = self.client.post('/authentication/register/', token, format='json') + self.assertEqual(response.status_code, 401) + + def test_register_bad_request(self): + data = {'username': 'admin', 'password': 'admin'} + response = self.client.post('/authentication/login/', data, format='json') + self.assertEqual(response.status_code, 200) + token = response.json() + + token.update({'username': 'user1'}) + response = self.client.post('/authentication/register/', token, format='json') + self.assertEqual(response.status_code, 400) + + def test_register_user_already_exist(self): + data = {'username': 'admin', 'password': 'admin'} + response = self.client.post('/authentication/login/', data, format='json') + self.assertEqual(response.status_code, 200) + token = response.json() + + token.update(data) + response = self.client.post('/authentication/register/', token, format='json') + self.assertEqual(response.status_code, 400) + + def test_register(self): + data = {'username': 'admin', 'password': 'admin'} + response = self.client.post('/authentication/login/', data, format='json') + self.assertEqual(response.status_code, 200) + token = response.json() + + token.update({'username': 'user1', 'password': 'pwd1'}) + response = self.client.post('/authentication/register/', token, format='json') + self.assertEqual(response.status_code, 201) + self.assertEqual( + sorted(list(response.json().keys())), + ['token', 'user_pk'] + ) diff --git a/decide/authentication/urls.py b/decide/authentication/urls.py index 5099cc8095..d05bfed6fc 100644 --- a/decide/authentication/urls.py +++ b/decide/authentication/urls.py @@ -1,11 +1,12 @@ from django.urls import include, path from rest_framework.authtoken.views import obtain_auth_token -from .views import GetUserView, LogoutView +from .views import GetUserView, LogoutView, RegisterView urlpatterns = [ path('login/', obtain_auth_token), path('logout/', LogoutView.as_view()), path('getuser/', GetUserView.as_view()), + path('register/', RegisterView.as_view()), ] diff --git a/decide/authentication/views.py b/decide/authentication/views.py index 5beab1e3dc..7825be67c2 100644 --- a/decide/authentication/views.py +++ b/decide/authentication/views.py @@ -1,6 +1,13 @@ from rest_framework.response import Response +from rest_framework.status import ( + HTTP_201_CREATED, + HTTP_400_BAD_REQUEST, + HTTP_401_UNAUTHORIZED +) from rest_framework.views import APIView from rest_framework.authtoken.models import Token +from django.contrib.auth.models import User +from django.db import IntegrityError from django.shortcuts import get_object_or_404 from django.core.exceptions import ObjectDoesNotExist @@ -24,3 +31,25 @@ def post(self, request): pass return Response({}) + + +class RegisterView(APIView): + def post(self, request): + key = request.data.get('token', '') + tk = get_object_or_404(Token, key=key) + if not tk.user.is_superuser: + return Response({}, status=HTTP_401_UNAUTHORIZED) + + username = request.data.get('username', '') + pwd = request.data.get('password', '') + if not username or not pwd: + return Response({}, status=HTTP_400_BAD_REQUEST) + + try: + user = User(username=username) + user.set_password(pwd) + user.save() + token, _ = Token.objects.get_or_create(user=user) + except IntegrityError: + return Response({}, status=HTTP_400_BAD_REQUEST) + return Response({'user_pk': user.pk, 'token': token.key}, HTTP_201_CREATED) diff --git a/decide/booth/static/booth/style.css b/decide/booth/static/booth/style.css new file mode 100644 index 0000000000..e5aa677cad --- /dev/null +++ b/decide/booth/static/booth/style.css @@ -0,0 +1,4 @@ +.voting { + margin-top: 20px; + margin-left: 40px; +} diff --git a/decide/booth/templates/booth/booth.html b/decide/booth/templates/booth/booth.html index 2933af171b..164f547a31 100644 --- a/decide/booth/templates/booth/booth.html +++ b/decide/booth/templates/booth/booth.html @@ -1,30 +1,70 @@ {% extends "base.html" %} {% load i18n static %} -{% block content %} -

{{ voting.id }} - {{ voting.name }}

- -
- - -
- - -
- -
- -
- {% trans "logout" %} -

{{ voting.question.desc }}

- - {% for opt in voting.question.options %} - - {% endfor %} +{% block extrahead %} + + + +{% endblock %} - +{% block content %} +
+ + + Decide + + + {% trans "logout" %} + + + + + + [[ alertMsg ]] + + +
+

[[ voting.id ]] - [[ voting.name ]]

+ + + + + + + + + + {% trans "Login" %} + + + +
+

[[ voting.question.desc ]]

+ + + [[ opt.option ]] + + + + {% trans "Vote" %} + +
+
{% endblock %} @@ -40,133 +80,134 @@

{{ voting.question.desc }}

- + + - function postData(url, data) { - // Default options are marked with * - var fdata = { - body: JSON.stringify(data), - headers: { - 'content-type': 'application/json', + {% endblock %} diff --git a/decide/booth/views.py b/decide/booth/views.py index a662587ce8..7e560d2706 100644 --- a/decide/booth/views.py +++ b/decide/booth/views.py @@ -1,3 +1,4 @@ +import json from django.views.generic import TemplateView from django.conf import settings from django.http import Http404 @@ -15,12 +16,16 @@ def get_context_data(self, **kwargs): try: r = mods.get('voting', params={'id': vid}) - context['voting'] = r[0] + + # Casting numbers to string to manage in javascript with BigInt + # and avoid problems with js and big number conversion + for k, v in r[0]['pub_key'].items(): + r[0]['pub_key'][k] = str(v) + + context['voting'] = json.dumps(r[0]) except: raise Http404 - context['store_url'] = settings.APIS.get('store', settings.BASEURL) - context['auth_url'] = settings.APIS.get('authentication', settings.BASEURL) context['KEYBITS'] = settings.KEYBITS return context diff --git a/decide/config.jsonnet.example b/decide/config.jsonnet.example new file mode 100644 index 0000000000..ec8bee4bd5 --- /dev/null +++ b/decide/config.jsonnet.example @@ -0,0 +1,59 @@ +# Here you can find the configuration for the decide server. This is a jsonnet +# file, it's a data template language, to learn more go to: +# https://jsonnet.org/ + +local host = "localhost"; +local port = "8000"; +local db = { + name: "decide", + user: "decide", + password: "decide", +}; + +{ + DEBUG: false, + SECRET_KEY: "^##ydkswfu0+=ofw0l#$kv^8n)0$i(qd&d&ol#p9!b$8*5%j1+", + KEYBITS: 256, + ALLOWED_VERSIONS: ["v1", "v2"], + DEFAULT_VERSION: "v1", + BASEURL: "http://" + host + ":" + port, + + # Modules in use, commented modules that you won"t use + MODULES: [ + "authentication", + "base", + "booth", + "census", + "mixnet", + "postproc", + "store", + "visualizer", + "voting", + ], + + # Endpoint for each module, if the module is served by this instance you + # can use localhost + APIS: { + "authentication": $["BASEURL"], + "base": $["BASEURL"], + "booth": $["BASEURL"], + "census": $["BASEURL"], + "mixnet": $["BASEURL"], + "postproc": $["BASEURL"], + "store": $["BASEURL"], + "visualizer": $["BASEURL"], + "voting": $["BASEURL"], + }, + + DATABASES: { + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": db["name"], + "USER": db["user"], + "PASSWORD": db["password"], + "HOST": host, + "PORT": "5432", + } + } +} + diff --git a/decide/decide/settings.py b/decide/decide/settings.py index e55d7363e1..1d22b67324 100644 --- a/decide/decide/settings.py +++ b/decide/decide/settings.py @@ -43,13 +43,15 @@ 'rest_framework', 'rest_framework.authtoken', 'rest_framework_swagger', + 'gateway', ] REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.BasicAuthentication', 'rest_framework.authentication.TokenAuthentication', - ) + ), + 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.QueryParameterVersioning' } AUTHENTICATION_BACKENDS = [ @@ -106,8 +108,12 @@ DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'decide', + 'USER': 'decide', + 'PASSWORD': 'decide', + 'HOST': 'localhost', + 'PORT': '5432', } } @@ -145,6 +151,8 @@ USE_TZ = True +TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' + # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.0/howto/static-files/ @@ -153,10 +161,22 @@ # number of bits for the key, all auths should use the same number of bits KEYBITS = 256 +# Versioning +ALLOWED_VERSIONS = ['v1', 'v2'] +DEFAULT_VERSION = 'v1' + try: from local_settings import * except ImportError: print("local_settings.py not found") +# loading jsonnet config +if os.path.exists("config.jsonnet"): + import json + from _jsonnet import evaluate_file + config = json.loads(evaluate_file("config.jsonnet")) + for k, v in config.items(): + vars()[k] = v + INSTALLED_APPS = INSTALLED_APPS + MODULES diff --git a/decide/decide/urls.py b/decide/decide/urls.py index 384044224b..d73f3cdb5d 100644 --- a/decide/decide/urls.py +++ b/decide/decide/urls.py @@ -23,7 +23,8 @@ urlpatterns = [ path('admin/', admin.site.urls), - path('doc/', schema_view) + path('doc/', schema_view), + path('gateway/', include('gateway.urls')), ] for module in settings.MODULES: diff --git a/decide/gateway/__init__.py b/decide/gateway/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/decide/gateway/admin.py b/decide/gateway/admin.py new file mode 100644 index 0000000000..8c38f3f3da --- /dev/null +++ b/decide/gateway/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/decide/gateway/apps.py b/decide/gateway/apps.py new file mode 100644 index 0000000000..3143e0f1ad --- /dev/null +++ b/decide/gateway/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class GatewayConfig(AppConfig): + name = 'gateway' diff --git a/decide/gateway/migrations/__init__.py b/decide/gateway/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/decide/gateway/models.py b/decide/gateway/models.py new file mode 100644 index 0000000000..71a8362390 --- /dev/null +++ b/decide/gateway/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/decide/gateway/tests.py b/decide/gateway/tests.py new file mode 100644 index 0000000000..7ce503c2dd --- /dev/null +++ b/decide/gateway/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/decide/gateway/urls.py b/decide/gateway/urls.py new file mode 100644 index 0000000000..e21cab3815 --- /dev/null +++ b/decide/gateway/urls.py @@ -0,0 +1,7 @@ +from django.urls import path +from . import views + + +urlpatterns = [ + path('', views.Gateway.as_view(), name='gateway'), +] diff --git a/decide/gateway/views.py b/decide/gateway/views.py new file mode 100644 index 0000000000..d07e97104d --- /dev/null +++ b/decide/gateway/views.py @@ -0,0 +1,18 @@ +from rest_framework.views import APIView +from rest_framework.response import Response +from base import mods + + +class Gateway(APIView): + def get(self, request, submodule, route): + kwargs = {'HTTP_AUTHORIZATION': request.META.get('HTTP_AUTHORIZATION', '')} + kwargs['params'] = {k: v for k, v in request.data.items()} + resp = mods.query(submodule, route, method='get', response=True, **kwargs) + return Response(resp.json(), status=resp.status_code) + + def post(self, request, submodule, route): + kwargs = {'HTTP_AUTHORIZATION': request.META.get('HTTP_AUTHORIZATION', '')} + kwargs['json'] = {k: v for k, v in request.data.items()} + + resp = mods.query(submodule, route, method='post', response=True, **kwargs) + return Response(resp.json(), status=resp.status_code) diff --git a/decide/visualizer/templates/visualizer/visualizer.html b/decide/visualizer/templates/visualizer/visualizer.html index 7edc0d62a7..0faed6bac3 100644 --- a/decide/visualizer/templates/visualizer/visualizer.html +++ b/decide/visualizer/templates/visualizer/visualizer.html @@ -1,20 +1,66 @@ {% extends "base.html" %} {% load i18n static %} +{% block extrahead %} + + + +{% endblock %} + {% block content %} -

{{ voting.id }} - {{ voting.name }}

- - {% if not voting.start_date %} -

{% trans 'Voting not started' %}

- {% elif not voting.end_date %} -

{% trans 'Voting is opened' %}

- {% else %} -

{% trans 'Results: ' %}

-
    - {% for option in voting.postproc %} -
  • {{option.option}}: {{option.postproc}} ({{option.votes}})
  • - {% endfor %} -
- {% endif %} +
+ + + Decide + + +
+

[[ voting.id ]] - [[ voting.name ]]

+ +

Votación no comenzada

+

Votación en curso

+
+

Resultados:

+ + + + + + + + + + + + + + + + +
OpciónPuntuaciónVotos
[[opt.option]][[opt.postproc]][[opt.votes]]
+
+ +
+
+{% endblock %} + +{% block extrabody %} + + + + + + {% endblock %} diff --git a/decide/visualizer/views.py b/decide/visualizer/views.py index 510ed66ac2..8fea64ecb2 100644 --- a/decide/visualizer/views.py +++ b/decide/visualizer/views.py @@ -1,3 +1,4 @@ +import json from django.views.generic import TemplateView from django.conf import settings from django.http import Http404 @@ -14,7 +15,7 @@ def get_context_data(self, **kwargs): try: r = mods.get('voting', params={'id': vid}) - context['voting'] = r[0] + context['voting'] = json.dumps(r[0]) except: raise Http404 diff --git a/decide/voting/serializers.py b/decide/voting/serializers.py index fab762b2b5..0713519528 100644 --- a/decide/voting/serializers.py +++ b/decide/voting/serializers.py @@ -26,3 +26,11 @@ class Meta: model = Voting fields = ('id', 'name', 'desc', 'question', 'start_date', 'end_date', 'pub_key', 'auths', 'tally', 'postproc') + + +class SimpleVotingSerializer(serializers.HyperlinkedModelSerializer): + question = QuestionSerializer(many=False) + + class Meta: + model = Voting + fields = ('name', 'desc', 'question', 'start_date', 'end_date') diff --git a/decide/voting/views.py b/decide/voting/views.py index 12061ed975..2f2227edc9 100644 --- a/decide/voting/views.py +++ b/decide/voting/views.py @@ -6,7 +6,7 @@ from rest_framework.response import Response from .models import Question, QuestionOption, Voting -from .serializers import VotingSerializer +from .serializers import SimpleVotingSerializer, VotingSerializer from base.perms import UserIsStaff from base.models import Auth @@ -18,6 +18,12 @@ class VotingView(generics.ListCreateAPIView): filter_fields = ('id', ) def get(self, request, *args, **kwargs): + version = request.version + if version not in settings.ALLOWED_VERSIONS: + version = settings.DEFAULT_VERSION + if version == 'v2': + self.serializer_class = SimpleVotingSerializer + return super().get(request, *args, **kwargs) def post(self, request, *args, **kwargs): diff --git a/doc/vmnv-31.10.11.pdf b/doc/vmnv-31.10.11.pdf new file mode 100644 index 0000000000..1c91f09f27 Binary files /dev/null and b/doc/vmnv-31.10.11.pdf differ diff --git a/loadtest/gen_census.py b/loadtest/gen_census.py new file mode 100644 index 0000000000..64f5705604 --- /dev/null +++ b/loadtest/gen_census.py @@ -0,0 +1,51 @@ +import json +import requests + + +HOST = "http://localhost:8000" +USER = "admin" +PASS = "admin" +VOTING = 1 + + +def create_voters(filename): + """ + Create voters with requests library from filename.json, where key are + usernames and values are the passwords. + """ + with open(filename) as f: + voters = json.loads(f.read()) + + data = {'username': USER, 'password': PASS} + response = requests.post(HOST + '/authentication/login/', data=data) + token = response.json() + + voters_pk = [] + invalid_voters = [] + for username, pwd in voters.items(): + token.update({'username': username, 'password': pwd}) + response = requests.post(HOST + '/authentication/register/', data=token) + if response.status_code == 201: + voters_pk.append(response.json().get('user_pk')) + else: + invalid_voters.append(username) + return voters_pk, invalid_voters + + +def add_census(voters_pk, voting_pk): + """ + Add to census all voters_pk in the voting_pk. + """ + data = {'username': USER, 'password': PASS} + response = requests.post(HOST + '/authentication/login/', data=data) + token = response.json() + + data2 = {'voters': voters_pk, 'voting_id': voting_pk} + auth = {'Authorization': 'Token ' + token.get('token')} + response = requests.post(HOST + '/census/', json=data2, headers=auth) + + + +voters, invalids = create_voters('voters.json') +add_census(voters, VOTING) +print("Create voters with pk={0} \nInvalid usernames={1}".format(voters, invalids)) diff --git a/loadtest/locustfile.py b/loadtest/locustfile.py new file mode 100644 index 0000000000..1e7ceb119f --- /dev/null +++ b/loadtest/locustfile.py @@ -0,0 +1,71 @@ +import json + +from random import choice + +from locust import ( + HttpLocust, + TaskSequence, + TaskSet, + seq_task, + task, +) + + +HOST = "http://localhost:8000" +VOTING = 1 + + +class DefVisualizer(TaskSet): + + @task + def index(self): + self.client.get("/visualizer/{0}/".format(VOTING)) + + +class DefVoters(TaskSequence): + + def on_start(self): + with open('voters.json') as f: + self.voters = json.loads(f.read()) + self.voter = choice(list(self.voters.items())) + + def on_quit(self): + self.voter = None + + @seq_task(1) + def login(self): + username, pwd = self.voter + self.token = self.client.post("/authentication/login/", { + "username": username, + "password": pwd, + }).json() + + @seq_task(2) + def getuser(self): + self.user = self.client.post("/authentication/getuser/", self.token).json() + + @seq_task(3) + def voting(self): + headers = { + 'Authorization': 'Token ' + self.token.get('token'), + 'content-type': 'application/json' + } + self.client.post("/store/", json.dumps({ + "token": self.token.get('token'), + "vote": { + "a": "12", + "b": "64" + }, + "voter": self.user.get('id'), + "voting": VOTING + }), headers=headers) + + +class Visualizer(HttpLocust): + host = HOST + task_set = DefVisualizer + + +class Voters(HttpLocust): + host = HOST + task_set = DefVoters diff --git a/loadtest/voters.json b/loadtest/voters.json new file mode 100644 index 0000000000..4cdcd66bfe --- /dev/null +++ b/loadtest/voters.json @@ -0,0 +1,23 @@ +{ + "username0": "password", + "username1": "password", + "username2": "password", + "username3": "password", + "username4": "password", + "username5": "password", + "username6": "password", + "username7": "password", + "username8": "password", + "username9": "password", + "username10": "password", + "username11": "password", + "username12": "password", + "username13": "password", + "username14": "password", + "username15": "password", + "username16": "password", + "username17": "password", + "username18": "password", + "username19": "password", + "username20": "password" +} diff --git a/requirements.txt b/requirements.txt index 3f4f228d6a..d5860a1eb4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,6 @@ requests==2.18.4 django-filter==1.1.0 psycopg2==2.7.4 django-rest-swagger==2.2.0 +coverage==4.5.2 +django-nose==1.4.6 +jsonnet==0.12.1 diff --git a/vagrant/Vagrantfile b/vagrant/Vagrantfile new file mode 100644 index 0000000000..87c50e7375 --- /dev/null +++ b/vagrant/Vagrantfile @@ -0,0 +1,80 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +# All Vagrant configuration is done below. The "2" in Vagrant.configure +# configures the configuration version (we support older styles for +# backwards compatibility). Please don't change it unless you know what +# you're doing. +Vagrant.configure("2") do |config| + # The most common configuration options are documented and commented below. + # For a complete reference, please see the online documentation at + # https://docs.vagrantup.com. + + # Every Vagrant development environment requires a box. You can search for + # boxes at https://vagrantcloud.com/search. + config.vm.box = "ubuntu/bionic64" + + # Disable automatic box update checking. If you disable this, then + # boxes will only be checked for updates when the user runs + # `vagrant box outdated`. This is not recommended. + # config.vm.box_check_update = false + + # Create a forwarded port mapping which allows access to a specific port + # within the machine from a port on the host machine. In the example below, + # accessing "localhost:8080" will access port 80 on the guest machine. + # NOTE: This will enable public access to the opened port + config.vm.network "forwarded_port", guest: 80, host: 8080 + + # Create a forwarded port mapping which allows access to a specific port + # within the machine from a port on the host machine and only allow access + # via 127.0.0.1 to disable public access + # config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1" + + # Create a private network, which allows host-only access to the machine + # using a specific IP. + # config.vm.network "private_network", ip: "192.168.33.10" + + # Create a public network, which generally matched to bridged network. + # Bridged networks make the machine appear as another physical device on + # your network. + # config.vm.network "public_network" + + # Share an additional folder to the guest VM. The first argument is + # the path on the host to the actual folder. The second argument is + # the path on the guest to mount the folder. And the optional third + # argument is a set of non-required options. + # config.vm.synced_folder "../data", "/vagrant_data" + + # Provider-specific configuration so you can fine-tune various + # backing providers for Vagrant. These expose provider-specific options. + # Example for VirtualBox: + # + # config.vm.provider "virtualbox" do |vb| + # # Display the VirtualBox GUI when booting the machine + # vb.gui = true + # + # # Customize the amount of memory on the VM: + # vb.memory = "1024" + # end + + config.vm.provider "virtualbox" do |v| + v.memory = 512 + v.cpus = 1 + end + + # View the documentation for the provider you are using for more + # information on available options. + + # Enable provisioning with a shell script. Additional provisioners such as + # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the + # documentation for more information about their specific syntax and use. + # config.vm.provision "shell", inline: <<-SHELL + # apt-get update + # apt-get install -y apache2 + # SHELL + config.vm.provision "ansible" do |ansible| + ansible.compatibility_mode = '2.0' + ansible.playbook = "playbook.yml" + ansible.extra_vars = { ansible_python_interpreter:"/usr/bin/python3" } + end +end diff --git a/vagrant/database.yml b/vagrant/database.yml new file mode 100644 index 0000000000..13cc476b55 --- /dev/null +++ b/vagrant/database.yml @@ -0,0 +1,16 @@ +--- +- name: Database + become: true + become_user: postgres + postgresql_db: + name: decide + +- name: Database user + become: true + become_user: postgres + postgresql_user: + db: decide + name: decide + password: decide + priv: ALL + diff --git a/vagrant/django.yml b/vagrant/django.yml new file mode 100644 index 0000000000..4405a29549 --- /dev/null +++ b/vagrant/django.yml @@ -0,0 +1,21 @@ +--- +- name: Collect static + become: yes + become_user: decide + shell: ~/venv/bin/python manage.py collectstatic --noinput + args: + chdir: /home/decide/decide/decide + +- name: Database migration + become: yes + become_user: decide + shell: ~/venv/bin/python manage.py migrate --noinput + args: + chdir: /home/decide/decide/decide + +- name: Admin superuser + become: yes + become_user: decide + shell: ~/venv/bin/python manage.py shell -c "from django.contrib.auth.models import User; User.objects.filter(username='admin') or User.objects.create_superuser('admin', 'admin@example.com', 'admin')" + args: + chdir: /home/decide/decide/decide diff --git a/vagrant/files.yml b/vagrant/files.yml new file mode 100644 index 0000000000..0ce3739d96 --- /dev/null +++ b/vagrant/files.yml @@ -0,0 +1,21 @@ +--- +- name: decide sysmtemd service + become: yes + become_user: root + copy: + src: files/decide.service + dest: /etc/systemd/system/decide.service + +- name: nginx.conf file + become: yes + become_user: root + copy: + src: files/nginx.conf + dest: /etc/nginx/conf.d/default.conf + +- name: django local_settings.py + become: yes + become_user: decide + copy: + src: files/settings.py + dest: /home/decide/decide/decide/local_settings.py diff --git a/vagrant/files/decide.service b/vagrant/files/decide.service new file mode 100644 index 0000000000..1e0f042956 --- /dev/null +++ b/vagrant/files/decide.service @@ -0,0 +1,13 @@ +[Unit] +Description=decide + +[Service] +User=decide +Type=simple +PIDFile=/var/run/decide.pid +WorkingDirectory=/home/decide/decide/decide +ExecStart=/home/decide/venv/bin/gunicorn -w 5 decide.wsgi --timeout=500 -b 0.0.0.0:8000 +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/vagrant/files/nginx.conf b/vagrant/files/nginx.conf new file mode 100644 index 0000000000..d9a242f973 --- /dev/null +++ b/vagrant/files/nginx.conf @@ -0,0 +1,24 @@ +server { + listen 80; + + server_name localhost; + root /home/decide/; + + location / { + include fastcgi_params; + proxy_pass http://localhost:8000; + proxy_redirect off; + + proxy_connect_timeout 500; + proxy_read_timeout 500; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + location /static { + autoindex on; + alias /home/decide/static; + } +} diff --git a/vagrant/files/settings.py b/vagrant/files/settings.py new file mode 100644 index 0000000000..1b7a4eab62 --- /dev/null +++ b/vagrant/files/settings.py @@ -0,0 +1,43 @@ +DEBUG = True + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'decide', + 'USER': 'decide', + 'PASSWORD': 'decide', + 'HOST': 'localhost', + 'PORT': 5432, + } +} + +STATIC_ROOT = '/home/decide/static/' +MEDIA_ROOT = '/home/decide/static/media/' +ALLOWED_HOSTS = ['*'] + +# Modules in use, commented modules that you won't use +MODULES = [ + 'authentication', + 'base', + 'booth', + 'census', + 'mixnet', + 'postproc', + 'store', + 'visualizer', + 'voting', +] + +BASEURL = 'http://localhost:8000' + +APIS = { + 'authentication': 'http://localhost:8000', + 'base': 'http://localhost:8000', + 'booth': 'http://localhost:8000', + 'census': 'http://localhost:8000', + 'mixnet': 'http://localhost:8000', + 'postproc': 'http://localhost:8000', + 'store': 'http://localhost:8000', + 'visualizer': 'http://localhost:8000', + 'voting': 'http://localhost:8000', +} diff --git a/vagrant/packages.yml b/vagrant/packages.yml new file mode 100644 index 0000000000..dc53d1886b --- /dev/null +++ b/vagrant/packages.yml @@ -0,0 +1,22 @@ +--- +- name: Install packages + become: true + apt: + name: "{{ packages }}" + update_cache: yes + vars: + packages: + - git + - postgresql + - python3 + - python3-pip + - python3-psycopg2 + - python3-virtualenv + - virtualenv + - nginx + - libpq-dev + - python-setuptools + - build-essential + - python-dev + - make + - m4 diff --git a/vagrant/playbook.yml b/vagrant/playbook.yml new file mode 100644 index 0000000000..fc07fd762d --- /dev/null +++ b/vagrant/playbook.yml @@ -0,0 +1,17 @@ +--- +- hosts: all + + tasks: + - include: packages.yml + tags: ["packages"] + - include: user.yml + - include: python.yml + tags: ["app"] + - include: files.yml + tags: ["files"] + - include: database.yml + tags: ["database"] + - include: django.yml + tags: ["django"] + - include: services.yml + tags: ["services"] diff --git a/vagrant/python.yml b/vagrant/python.yml new file mode 100644 index 0000000000..3828a12c98 --- /dev/null +++ b/vagrant/python.yml @@ -0,0 +1,24 @@ +--- +- name: Git clone + become: yes + become_user: decide + git: + repo: 'https://github.com/wadobo/decide.git' + dest: /home/decide/decide + version: master + +- name: Python virtualenv + become: yes + become_user: decide + pip: + name: "gunicorn" + virtualenv: /home/decide/venv + virtualenv_python: python3 + +- name: Requirements + become: yes + become_user: decide + pip: + requirements: /home/decide/decide/requirements.txt + virtualenv: /home/decide/venv + virtualenv_python: python3 diff --git a/vagrant/services.yml b/vagrant/services.yml new file mode 100644 index 0000000000..18cbe3dab9 --- /dev/null +++ b/vagrant/services.yml @@ -0,0 +1,12 @@ +--- +- name: Starting services + become: yes + become_user: root + systemd: + state: restarted + enabled: yes + name: "{{ item }}" + loop: + - postgresql + - nginx + - decide diff --git a/vagrant/user.yml b/vagrant/user.yml new file mode 100644 index 0000000000..87c8a19462 --- /dev/null +++ b/vagrant/user.yml @@ -0,0 +1,7 @@ +--- +- name: Create decide user + become: true + user: + name: decide + comment: Decide app user + state: present