diff --git a/Dockerfile b/Dockerfile index 4472a7837250cabd6b32f87731ff63f80292ba95..137fba83027d04a5987961f75793a60186b7033b 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 0000000000000000000000000000000000000000..0b555c966c18d02f2f312313464a0d8ede1a1673 --- /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 0000000000000000000000000000000000000000..45b64e7d5b47855ebbc08d8755ff2f3bf826f557 --- /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 0000000000000000000000000000000000000000..60f19878e014f3d94172b9f37f8f39f234636245 --- /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 324817c9c7b7d7f22a9b0f65408ccbb0ca8782f6..d860f56880919a39b2ea7de681c18a88e785caec 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 e35efd23de762bd382e8bcdbacd1ed4f768d05b5..97f050776216516a473d97ecb5ff30c57fe828ab 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 0ca760efe9c3a468e3046dfa830ca216aff9eb99..dddc7b6d1ef49ed37b92d80a600f050befebaf5a 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