From 7eeb18f55c43db384f5f23ca91831e755599390a Mon Sep 17 00:00:00 2001
From: Michael Wedl <michael@syslifters.com>
Date: Wed, 30 Oct 2024 10:48:02 +0100
Subject: [PATCH] Plugin system bugfixes

---
 Dockerfile                                 |  5 ++--
 api/src/reportcreator_api/conf/plugins.py  | 30 ++++++++++++++--------
 api/src/reportcreator_api/conf/settings.py |  2 +-
 api/src/reportcreator_api/conf/urls.py     |  5 ++--
 api/start.sh                               |  5 ++++
 deploy/sysreptor/docker-compose.yml        |  1 +
 frontend/src/plugins/pluginLoader.ts       |  2 +-
 plugins/build.sh                           |  2 +-
 plugins/cyberchef/build.sh                 | 11 +++++---
 9 files changed, 42 insertions(+), 21 deletions(-)

diff --git a/Dockerfile b/Dockerfile
index e5ff9a3ab..b6a156787 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -210,10 +210,9 @@ FROM --platform=$BUILDPLATFORM api-test AS api-statics
 # Do not post-process nuxt files, because they already have hash names (and django failes to post-process them)
 RUN python3 manage.py collectstatic --no-input --clear
 COPY --from=frontend /app/frontend/dist/index.html /app/frontend/dist/static/ /app/api/frontend/static/
-RUN mv /app/api/frontend/static/index.html /app/api/frontend/index.html \
-    && python3 manage.py collectstatic --no-input --no-post-process
-
 COPY --from=plugin-builder --chown=user:user /app/plugins/ /app/plugins/
+RUN mv /app/api/frontend/static/index.html /app/api/frontend/index.html \
+    && ENABLED_PLUGINS='*' python3 manage.py collectstatic --no-input --no-post-process
 
 
 FROM api-test AS api
diff --git a/api/src/reportcreator_api/conf/plugins.py b/api/src/reportcreator_api/conf/plugins.py
index 45a7bcc5d..1c2244b84 100644
--- a/api/src/reportcreator_api/conf/plugins.py
+++ b/api/src/reportcreator_api/conf/plugins.py
@@ -122,7 +122,7 @@ def remove_entry(path: Path):
         path.unlink(missing_ok=True)
 
 
-def create_plugin_module_dir(dst: Path, srcs: list[Path]):
+def collect_plugins(dst: Path, srcs: list[Path]):
     # Collect plugins from all plugin directories
     all_module_dirs = []
     for plugins_dir in srcs:
@@ -166,10 +166,17 @@ def create_plugin_module_dir(dst: Path, srcs: list[Path]):
 def load_plugins(plugin_dirs: list[Path], enabled_plugins: list[str]):
     dst = Path(__file__).parent.parent.parent / 'sysreptor_plugins'
     # Collect all plugin modules in dst directory
-    create_plugin_module_dir(
-        dst=dst,
-        srcs=plugin_dirs,
-    )
+    try:
+        collect_plugins(
+            dst=dst,
+            srcs=plugin_dirs,
+        )
+    except Exception:
+        logging.exception('Error while collecting plugins')
+
+    if not dst.is_dir() or not (dst / '__init__.py').is_file():
+        logging.warning(f'Cannot load plugins: Plugin directory "{dst}" not found')
+        return []
 
     # Load sysreptor_plugins module from dst directory
     load_module_from_dir('sysreptor_plugins', dst / '__init__.py')
@@ -196,7 +203,8 @@ def load_plugins(plugin_dirs: list[Path], enabled_plugins: list[str]):
 
         plugin_config_class.name = module_name
         if any(c.plugin_id == plugin_config_class.plugin_id for c in available_plugin_configs):
-            raise ImproperlyConfigured(f'Duplicate plugin_id: {plugin_config_class.plugin_id}')
+            logging.warning(f'Duplicate plugin_id: {plugin_config_class.plugin_id}')
+            continue
 
         available_plugin_configs.append(plugin_config_class)
 
@@ -206,15 +214,17 @@ def load_plugins(plugin_dirs: list[Path], enabled_plugins: list[str]):
         for plugin_config_class in available_plugin_configs:
             plugin_id = plugin_config_class.plugin_id
             plugin_name = plugin_config_class.name.split('.')[-1]
-            if enabled_plugin_id in [plugin_id, plugin_name]:
+            if enabled_plugin_id in [plugin_id, plugin_name, '*']:
                 # Add to installed_apps
                 app_class = plugin_config_class.__module__ + '.' + plugin_config_class.__name__
                 app_label = plugin_config_class.label
                 if app_class not in installed_apps:
                     installed_apps.append(app_class)
-                    logging.info(f'Enabling plugin {plugin_name} ({plugin_id=}, {app_label=}, {app_class=})', file=sys.stderr)  # noqa: T201
-                break
+                    logging.info(f'Enabling plugin {plugin_name} ({plugin_id=}, {app_label=}, {app_class=})')
+                if enabled_plugin_id != '*':
+                    break
         else:
-            logging.warning(f'Plugin "{enabled_plugin_id}" not found in plugins')
+            if enabled_plugin_id != '*':
+                logging.warning(f'Plugin "{enabled_plugin_id}" not found in plugins')
 
     return installed_apps
diff --git a/api/src/reportcreator_api/conf/settings.py b/api/src/reportcreator_api/conf/settings.py
index 08cd228fa..c8148cfdb 100644
--- a/api/src/reportcreator_api/conf/settings.py
+++ b/api/src/reportcreator_api/conf/settings.py
@@ -101,7 +101,7 @@
 TEMPLATES = [
     {
         'BACKEND': 'django.template.backends.django.DjangoTemplates',
-        'DIRS': [BASE_DIR / 'frontend'],
+        'DIRS': [],
         'APP_DIRS': True,
         'OPTIONS': {
             'context_processors': [
diff --git a/api/src/reportcreator_api/conf/urls.py b/api/src/reportcreator_api/conf/urls.py
index dbcfeea38..26ec61a74 100644
--- a/api/src/reportcreator_api/conf/urls.py
+++ b/api/src/reportcreator_api/conf/urls.py
@@ -3,7 +3,8 @@
 from django.contrib import admin
 from django.http import HttpResponse
 from django.urls import include, path, re_path
-from django.views.generic.base import RedirectView, TemplateView
+from django.views.generic.base import RedirectView
+from django.views.static import serve
 from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerSplitView
 from rest_framework.routers import DefaultRouter
 from rest_framework_nested.routers import NestedSimpleRouter
@@ -167,7 +168,7 @@
     path('favicon.ico', RedirectView.as_view(url='/static/favicon.ico', permanent=True)),
 
     # Fallback URL for SPA
-    re_path(r'^(?!(api|admin)).*/?$', TemplateView.as_view(template_name='index.html')),
+    re_path(r'^(?!(api|admin|static)).*/?$', lambda request, *args, **kwargs: serve(request, path='index.html', document_root=settings.BASE_DIR / 'frontend')),
 ]
 
 
diff --git a/api/start.sh b/api/start.sh
index 2065da996..de229b584 100644
--- a/api/start.sh
+++ b/api/start.sh
@@ -1,10 +1,15 @@
 #!/bin/bash
+# Add custom CA certificates
 if [[ -n "$CA_CERTIFICATES" ]]; then
     echo "${CA_CERTIFICATES}" >> /usr/local/share/ca-certificates/custom-user-cert.crt
     update-ca-certificates
 fi
 
+# Run DB migrations
 python3 manage.py migrate
+# Collect static files (of custom plugins)
+python3 manage.py collectstatic --noinput --no-post-process
+# Start web application
 gunicorn --bind=:8000 \
          --worker-class=uvicorn.workers.UvicornWorker \
          --workers=${SERVER_WORKERS} \
diff --git a/deploy/sysreptor/docker-compose.yml b/deploy/sysreptor/docker-compose.yml
index 7add1d5c0..ba3e73ddb 100644
--- a/deploy/sysreptor/docker-compose.yml
+++ b/deploy/sysreptor/docker-compose.yml
@@ -34,6 +34,7 @@ services:
     restart: unless-stopped
   app:
     image: 'syslifters/sysreptor:${SYSREPTOR_VERSION:-latest}'
+    # build: ../../
     container_name: 'sysreptor-app'
     init: true
     volumes:
diff --git a/frontend/src/plugins/pluginLoader.ts b/frontend/src/plugins/pluginLoader.ts
index 437b57259..3bc87268a 100644
--- a/frontend/src/plugins/pluginLoader.ts
+++ b/frontend/src/plugins/pluginLoader.ts
@@ -72,7 +72,7 @@ export function usePluginHelpers(pluginHelperOptions: { pluginConfig: PluginConf
         });
       }
       let iframeSrc = attrs.src;
-      if (!iframeSrc.startsWith('/')) {
+      if (!iframeSrc.startsWith('/') && !iframeSrc.startsWith('https://') && !iframeSrc.startsWith('http://')) {
         // Convert relative src to absolute
         if (iframeSrc.startsWith('api/')) {
           iframeSrc = `/api/plugins/${pluginHelperOptions.pluginConfig.id}/${iframeSrc}`;
diff --git a/plugins/build.sh b/plugins/build.sh
index 88df3338c..97093dcb6 100755
--- a/plugins/build.sh
+++ b/plugins/build.sh
@@ -1,4 +1,4 @@
-#!/bin/sh
+#!/bin/bash
 set -e  # exit on error
 
 for plugin in ./*; do
diff --git a/plugins/cyberchef/build.sh b/plugins/cyberchef/build.sh
index 55a62af74..3e5203a92 100755
--- a/plugins/cyberchef/build.sh
+++ b/plugins/cyberchef/build.sh
@@ -1,9 +1,14 @@
-#!/bin/sh
+#!/bin/bash
 
-if [ ! -d static/cyberchef ]; then
+set -e
+
+CYBERCHEF_VERSION="v10.19.2"
+
+if [ ! -f "./static/cyberchef/CyberChef_${CYBERCHEF_VERSION}.html" ]; then
   echo "Downloading CyberChef"
+  rm -rf static/cyberchef/*
   mkdir -p static/cyberchef
-  curl https://github.com/gchq/CyberChef/releases/download/v10.19.2/CyberChef_v10.19.2.zip -o cyberchef.zip
+  curl -L "https://github.com/gchq/CyberChef/releases/download/v10.19.2/CyberChef_${CYBERCHEF_VERSION}.zip" -o cyberchef.zip
   unzip cyberchef.zip -d static/cyberchef
   rm cyberchef.zip
 else