From ae8496992b03a8d9e5546cb14ecb2d72f8f9e075 Mon Sep 17 00:00:00 2001 From: Erwan Rouchet <rouchet@teklia.com> Date: Mon, 18 Nov 2019 11:02:51 +0000 Subject: [PATCH] Base Channels setup --- Dockerfile | 8 +++--- arkindex/project/asgi.py | 13 ++++++++++ arkindex/project/consumers.py | 46 +++++++++++++++++++++++++++++++++++ arkindex/project/routing.py | 12 +++++++++ arkindex/project/settings.py | 20 +++++++++++++++ base/requirements.txt | 2 ++ requirements.txt | 2 ++ 7 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 arkindex/project/asgi.py create mode 100644 arkindex/project/consumers.py create mode 100644 arkindex/project/routing.py diff --git a/Dockerfile b/Dockerfile index 4472a78372..137fba8302 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM registry.gitlab.com/arkindex/backend:base-0.10.1 +FROM registry.gitlab.com/arkindex/backend:base-0.10.3 ARG COMMON_BRANCH=master ARG COMMON_ID=9855787 @@ -26,12 +26,12 @@ RUN \ # Install arkindex and its deps # Uses a source archive instead of full local copy to speedup docker build COPY dist/arkindex-*.tar.gz /tmp/arkindex.tar.gz -RUN pip install /tmp/arkindex.tar.gz gunicorn==19.9 && rm /tmp/arkindex.tar.gz +RUN pip install /tmp/arkindex.tar.gz && rm /tmp/arkindex.tar.gz # Allow access to logs RUN mkdir -p /logs RUN chown -R ark:teklia /logs -# Run through supervisor +# Run with Daphne EXPOSE 80 -CMD ["gunicorn", "--access-logfile=-", "--capture-output", "--bind=0.0.0.0:80", "arkindex.project.wsgi"] +CMD ["daphne", "--verbosity=1", "--bind=0.0.0.0", "--port=80", "arkindex.project.asgi:application"] diff --git a/arkindex/project/asgi.py b/arkindex/project/asgi.py new file mode 100644 index 0000000000..0b555c966c --- /dev/null +++ b/arkindex/project/asgi.py @@ -0,0 +1,13 @@ +""" +ASGI entrypoint. Configures Django and then runs the application +defined in the ASGI_APPLICATION setting. +""" + +import os +import django +from channels.routing import get_default_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "arkindex.project.settings") +os.environ['ALL_CHECKS'] = 'true' +django.setup() +application = get_default_application() diff --git a/arkindex/project/consumers.py b/arkindex/project/consumers.py new file mode 100644 index 0000000000..45b64e7d5b --- /dev/null +++ b/arkindex/project/consumers.py @@ -0,0 +1,46 @@ +from channels.exceptions import DenyConnection +from channels.generic.websocket import AsyncJsonWebsocketConsumer + + +class ConsumerPermission(object): + """ + Base class for permission classes on Channels consumers. + """ + + def has_permission(self, consumer): + return True + + +class IsAuthenticated(ConsumerPermission): + """ + Restrict connections to authenticated users. + """ + + def has_permission(self, consumer): + if not consumer.user.is_authenticated: + return False + return super().has_permission(consumer) + + +class IsAdmin(ConsumerPermission): + """ + Restrict connections to admin users. + """ + + def has_permission(self, consumer): + if not consumer.user.is_authenticated or not consumer.user.is_admin: + return False + return super().has_permission(consumer) + + +class AuthConsumer(AsyncJsonWebsocketConsumer): + permission_classes = () + + async def connect(self): + self.user = self.scope["user"] + for permission_class in self.permission_classes: + if not permission_class().has_permission(self): + # Raise instead of using self.close() because it allows subclasses + # to call await super().connect() and continue only when connections are accepted + raise DenyConnection + await self.accept() diff --git a/arkindex/project/routing.py b/arkindex/project/routing.py new file mode 100644 index 0000000000..60f19878e0 --- /dev/null +++ b/arkindex/project/routing.py @@ -0,0 +1,12 @@ +from channels.auth import AuthMiddlewareStack +from channels.routing import ProtocolTypeRouter, URLRouter +from channels.security.websocket import AllowedHostsOriginValidator + +application = ProtocolTypeRouter({ + 'websocket': AllowedHostsOriginValidator( + AuthMiddlewareStack( + URLRouter([ + ]), + ), + ), +}) diff --git a/arkindex/project/settings.py b/arkindex/project/settings.py index 324817c9c7..d860f56880 100644 --- a/arkindex/project/settings.py +++ b/arkindex/project/settings.py @@ -117,6 +117,7 @@ INSTALLED_APPS = [ 'django_admin_hstore_widget', # Tools + 'channels', 'rest_framework', 'rest_framework.authtoken', 'django_filters', @@ -162,6 +163,7 @@ TEMPLATES = [ ] WSGI_APPLICATION = 'arkindex.project.wsgi.application' +ASGI_APPLICATION = 'arkindex.project.routing.application' # Password validation @@ -277,6 +279,18 @@ CACHES = { } } +# Django Channels layer using Redis +CHANNEL_LAYERS = { + "default": { + "BACKEND": "channels_redis.core.RedisChannelLayer", + "CONFIG": { + "hosts": [ + (os.environ.get('REDIS_HOST', 'localhost'), 6379) + ], + }, + }, +} + LOGGING = { 'version': 1, 'disable_existing_loggers': False, @@ -319,6 +333,10 @@ LOGGING = { 'level': 'INFO', 'propagate': True, }, + 'daphne.server': { + 'handlers': ['console_debug'], + 'level': 'INFO', + }, 'arkindex.documents.importer': { 'handlers': ['importers'], 'level': 'DEBUG', @@ -363,6 +381,8 @@ CSRF_COOKIE_DOMAIN = os.environ.get('COOKIE_DOMAIN') SESSION_COOKIE_NAME = 'arkindex.auth' SESSION_COOKIE_DOMAIN = os.environ.get('COOKIE_DOMAIN') +# Required for authentication over websockets +SESSION_COOKIE_HTTPONLY = False CORS_ORIGIN_WHITELIST = env2list('CORS_ORIGIN_WHITELIST', default=[ 'universalviewer.io', diff --git a/base/requirements.txt b/base/requirements.txt index e35efd23de..97f0507762 100644 --- a/base/requirements.txt +++ b/base/requirements.txt @@ -2,9 +2,11 @@ boto3==1.9 cryptography>=2.7 Django==2.2 elasticsearch==6.2.0 +hiredis==1.0.0 ijson==2.3 lxml==4.2.3 openpyxl==2.4.9 Pillow==4.3.0 psycopg2==2.7.3.2 python-Levenshtein==0.12.0 +Twisted==19.7.0 diff --git a/requirements.txt b/requirements.txt index 0ca760efe9..dddc7b6d1e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,8 @@ arkindex-common==0.2.0 certifi==2017.7.27.1 +channels==2.3.1 +channels-redis==2.4.1 chardet==3.0.4 django-admin-hstore-widget==1.0.1 django-cors-headers==2.4.0 -- GitLab