From 6f5bf247a0cf8a2ca9bea19a34fd60dd2aeb7c3a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Aug 2023 22:13:58 +0000 Subject: [PATCH 001/148] Bump apollo-server-core from 2.26.0 to 2.26.2 in /web-src Bumps [apollo-server-core](https://github.com/apollographql/apollo-server/tree/HEAD/packages/apollo-server-core) from 2.26.0 to 2.26.2. - [Release notes](https://github.com/apollographql/apollo-server/releases) - [Commits](https://github.com/apollographql/apollo-server/commits/apollo-server-core@2.26.2/packages/apollo-server-core) --- updated-dependencies: - dependency-name: apollo-server-core dependency-type: indirect ... Signed-off-by: dependabot[bot] --- web-src/package-lock.json | 61 ++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/web-src/package-lock.json b/web-src/package-lock.json index 9fc80a86..669f389c 100644 --- a/web-src/package-lock.json +++ b/web-src/package-lock.json @@ -1,12 +1,12 @@ { "name": "script-server", - "version": "1.17.0", + "version": "1.18.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "script-server", - "version": "1.17.0", + "version": "1.18.0", "dependencies": { "ace-builds": "^1.11.2", "axios": "^0.27.2", @@ -160,13 +160,13 @@ } }, "node_modules/@apollographql/graphql-upload-8-fork": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/@apollographql/graphql-upload-8-fork/-/graphql-upload-8-fork-8.1.3.tgz", - "integrity": "sha512-ssOPUT7euLqDXcdVv3Qs4LoL4BPtfermW1IOouaqEmj36TpHYDmYDIbKoSQxikd9vtMumFnP87OybH7sC9fJ6g==", + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/@apollographql/graphql-upload-8-fork/-/graphql-upload-8-fork-8.1.4.tgz", + "integrity": "sha512-lHAj/PUegYu02zza9Pg0bQQYH5I0ah1nyIzu2YIqOv41P0vu3GCBISAmQCfFHThK7N3dy7dLFPhoKcXlXRLPoQ==", "dev": true, "dependencies": { "@types/express": "*", - "@types/fs-capacitor": "*", + "@types/fs-capacitor": "^2.0.0", "@types/koa": "*", "busboy": "^0.3.1", "fs-capacitor": "^2.0.4", @@ -3669,9 +3669,9 @@ "dev": true }, "node_modules/@types/http-errors": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-1.8.2.tgz", - "integrity": "sha512-EqX+YQxINb+MeXaIqYDASb6U6FCHbWjkj4a1CKDBks3d/QiB2+PqBLyO72vLDgAO1wUI4O+9gweRcQK11bTL/w==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==", "dev": true }, "node_modules/@types/http-proxy": { @@ -4013,9 +4013,9 @@ "dev": true }, "node_modules/@types/koa": { - "version": "2.13.5", - "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.13.5.tgz", - "integrity": "sha512-HSUOdzKz3by4fnqagwthW/1w/yJspTgppyyalPVbgZf8jQWvdIXcVW5h2DGtw4zYntOaeRGx49r1hxoPWrD4aA==", + "version": "2.13.8", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.13.8.tgz", + "integrity": "sha512-Ugmxmgk/yPRW3ptBTh9VjOLwsKWJuGbymo1uGX0qdaqqL18uJiiG1ZoV0rxCOYSaDGhvEp5Ece02Amx0iwaxQQ==", "dev": true, "dependencies": { "@types/accepts": "*", @@ -5527,14 +5527,15 @@ "dev": true }, "node_modules/apollo-server-core": { - "version": "2.26.0", - "resolved": "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-2.26.0.tgz", - "integrity": "sha512-z0dAZGu6zLhYLWVaRis6pR1dQbzPhA6xU5z0issR/sQR5kr466vFMF/rq//Jqwpd/A4xfTXZrFmr5urFyl4k4g==", + "version": "2.26.2", + "resolved": "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-2.26.2.tgz", + "integrity": "sha512-r8jOhf1jElaxsNsALFMy/MLiJCqSa1ZiwxkerVYbsEkyWrpD1Khy0extDkTBrfa6uK8CatX7xK9U413bYNhJFA==", + "deprecated": "The `apollo-server-core` package is part of Apollo Server v2 and v3, which are now deprecated (end-of-life October 22nd 2023 and October 22nd 2024, respectively). This package's functionality is now found in the `@apollo/server` package. See https://www.apollographql.com/docs/apollo-server/previous-versions/ for more details.", "dev": true, "dependencies": { "@apollographql/apollo-tools": "^0.5.0", "@apollographql/graphql-playground-html": "1.6.27", - "@apollographql/graphql-upload-8-fork": "^8.1.3", + "@apollographql/graphql-upload-8-fork": "^8.1.4", "@josephg/resolvable": "^1.0.0", "@types/ws": "^7.0.0", "apollo-cache-control": "^0.15.0", @@ -25142,13 +25143,13 @@ } }, "@apollographql/graphql-upload-8-fork": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/@apollographql/graphql-upload-8-fork/-/graphql-upload-8-fork-8.1.3.tgz", - "integrity": "sha512-ssOPUT7euLqDXcdVv3Qs4LoL4BPtfermW1IOouaqEmj36TpHYDmYDIbKoSQxikd9vtMumFnP87OybH7sC9fJ6g==", + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/@apollographql/graphql-upload-8-fork/-/graphql-upload-8-fork-8.1.4.tgz", + "integrity": "sha512-lHAj/PUegYu02zza9Pg0bQQYH5I0ah1nyIzu2YIqOv41P0vu3GCBISAmQCfFHThK7N3dy7dLFPhoKcXlXRLPoQ==", "dev": true, "requires": { "@types/express": "*", - "@types/fs-capacitor": "*", + "@types/fs-capacitor": "^2.0.0", "@types/koa": "*", "busboy": "^0.3.1", "fs-capacitor": "^2.0.4", @@ -27705,9 +27706,9 @@ "dev": true }, "@types/http-errors": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-1.8.2.tgz", - "integrity": "sha512-EqX+YQxINb+MeXaIqYDASb6U6FCHbWjkj4a1CKDBks3d/QiB2+PqBLyO72vLDgAO1wUI4O+9gweRcQK11bTL/w==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==", "dev": true }, "@types/http-proxy": { @@ -27993,9 +27994,9 @@ "dev": true }, "@types/koa": { - "version": "2.13.5", - "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.13.5.tgz", - "integrity": "sha512-HSUOdzKz3by4fnqagwthW/1w/yJspTgppyyalPVbgZf8jQWvdIXcVW5h2DGtw4zYntOaeRGx49r1hxoPWrD4aA==", + "version": "2.13.8", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.13.8.tgz", + "integrity": "sha512-Ugmxmgk/yPRW3ptBTh9VjOLwsKWJuGbymo1uGX0qdaqqL18uJiiG1ZoV0rxCOYSaDGhvEp5Ece02Amx0iwaxQQ==", "dev": true, "requires": { "@types/accepts": "*", @@ -29292,14 +29293,14 @@ } }, "apollo-server-core": { - "version": "2.26.0", - "resolved": "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-2.26.0.tgz", - "integrity": "sha512-z0dAZGu6zLhYLWVaRis6pR1dQbzPhA6xU5z0issR/sQR5kr466vFMF/rq//Jqwpd/A4xfTXZrFmr5urFyl4k4g==", + "version": "2.26.2", + "resolved": "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-2.26.2.tgz", + "integrity": "sha512-r8jOhf1jElaxsNsALFMy/MLiJCqSa1ZiwxkerVYbsEkyWrpD1Khy0extDkTBrfa6uK8CatX7xK9U413bYNhJFA==", "dev": true, "requires": { "@apollographql/apollo-tools": "^0.5.0", "@apollographql/graphql-playground-html": "1.6.27", - "@apollographql/graphql-upload-8-fork": "^8.1.3", + "@apollographql/graphql-upload-8-fork": "^8.1.4", "@josephg/resolvable": "^1.0.0", "@types/ws": "^7.0.0", "apollo-cache-control": "^0.15.0", From a918ba19388e57ff4fdd2d359b3ebd162e72dfe5 Mon Sep 17 00:00:00 2001 From: Jason Boblick Date: Fri, 29 Sep 2023 10:51:19 -0400 Subject: [PATCH 002/148] make attachments optional for email alerts --- src/communications/destination_email.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/communications/destination_email.py b/src/communications/destination_email.py index 13731296..af3575d1 100644 --- a/src/communications/destination_email.py +++ b/src/communications/destination_email.py @@ -56,6 +56,7 @@ def __init__(self, params_dict): self.auth_enabled = read_bool_from_config('auth_enabled', params_dict) self.login = params_dict.get('login') self.tls = read_bool_from_config('tls', params_dict) + self.include_files = read_bool_from_config('include_files', params_dict, default=True) self.password = self.read_password(params_dict) self.to_addresses = split_addresses(self.to_addresses) @@ -103,7 +104,7 @@ def send(self, title, body, files=None): if self.auth_enabled: server.login(self.login, self.password) - if files: + if self.include_files and files: for file in files: filename = file.filename part = MIMEApplication(file.content, Name=filename) From bc6071cf65fb54e4c2e250ddf75d097a73dee5f9 Mon Sep 17 00:00:00 2001 From: Jason Boblick Date: Tue, 3 Oct 2023 14:04:01 -0400 Subject: [PATCH 003/148] rename variable to better match purpose --- src/communications/destination_email.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/communications/destination_email.py b/src/communications/destination_email.py index af3575d1..a53ca465 100644 --- a/src/communications/destination_email.py +++ b/src/communications/destination_email.py @@ -56,7 +56,7 @@ def __init__(self, params_dict): self.auth_enabled = read_bool_from_config('auth_enabled', params_dict) self.login = params_dict.get('login') self.tls = read_bool_from_config('tls', params_dict) - self.include_files = read_bool_from_config('include_files', params_dict, default=True) + self.attach_files = read_bool_from_config('attach_files', params_dict, default=True) self.password = self.read_password(params_dict) self.to_addresses = split_addresses(self.to_addresses) @@ -104,7 +104,7 @@ def send(self, title, body, files=None): if self.auth_enabled: server.login(self.login, self.password) - if self.include_files and files: + if self.attach_files and files: for file in files: filename = file.filename part = MIMEApplication(file.content, Name=filename) From 20370ff326c0291a8497d385a0ca3491cd001296 Mon Sep 17 00:00:00 2001 From: yshepilov Date: Sun, 15 Oct 2023 17:54:32 +0200 Subject: [PATCH 004/148] #699 added automatic script groups based on sub-folders --- src/config/config_service.py | 34 +++++++-- src/main.py | 3 +- src/model/script_config.py | 13 +++- src/model/server_conf.py | 20 +++++ src/tests/config_service_test.py | 73 ++++++++++++++++--- src/tests/execution_logging_test.py | 5 +- src/tests/execution_service_test.py | 17 +++-- src/tests/scheduling/schedule_service_test.py | 6 +- src/tests/script_config_test.py | 62 +++++++++++----- src/tests/test_utils.py | 30 ++++++-- src/tests/web/script_config_socket_test.py | 2 +- src/tests/web/server_test.py | 2 +- src/utils/file_utils.py | 36 +++++---- 13 files changed, 229 insertions(+), 74 deletions(-) diff --git a/src/config/config_service.py b/src/config/config_service.py index 9e5c1324..bcc4ebaa 100644 --- a/src/config/config_service.py +++ b/src/config/config_service.py @@ -3,6 +3,7 @@ import os import re import shutil +from datetime import datetime from typing import NamedTuple, Optional from auth.authorization import Authorizer @@ -14,8 +15,6 @@ from utils.file_utils import to_filename from utils.process_utils import ProcessInvoker from utils.string_utils import is_blank, strip -from datetime import datetime - SCRIPT_EDIT_CODE_MODE = 'new_code' SCRIPT_EDIT_UPLOAD_MODE = 'upload_script' @@ -56,12 +55,19 @@ def _create_archive_filename(filename): class ConfigService: - def __init__(self, authorizer, conf_folder, process_invoker: ProcessInvoker) -> None: + def __init__( + self, + authorizer, + conf_folder, + group_scripts_by_folder: bool, + process_invoker: ProcessInvoker) -> None: + self._authorizer = authorizer # type: Authorizer self._script_configs_folder = os.path.join(conf_folder, 'runners') self._scripts_folder = os.path.join(conf_folder, 'scripts') self._scripts_deleted_folder = os.path.join(conf_folder, 'deleted') self._process_invoker = process_invoker + self._group_scripts_by_folder = group_scripts_by_folder file_utils.prepare_folder(self._script_configs_folder) file_utils.prepare_folder(self._scripts_deleted_folder) @@ -117,7 +123,7 @@ def update_config(self, user, config, filename, uploaded_script): with open(original_file_path, 'r') as f: original_config_json = json.load(f) - short_original_config = script_config.read_short(original_file_path, original_config_json) + short_original_config = self.read_short_config(original_config_json, original_file_path) name = config['name'] @@ -133,6 +139,12 @@ def update_config(self, user, config, filename, uploaded_script): LOGGER.info('Updating script config "' + name + '" in ' + original_file_path) self._save_config(config, original_file_path) + def read_short_config(self, config_json, file_path): + return script_config.read_short( + file_path, + config_json, + self._group_scripts_by_folder, + self._script_configs_folder) def delete_config(self, user, name): self._check_admin_access(user) @@ -220,7 +232,7 @@ def list_configs(self, user, mode=None): def load_script(path, content) -> Optional[ShortConfig]: try: config_object = self.load_config_file(path, content) - short_config = script_config.read_short(path, config_object) + short_config = self.read_short_config(config_object, path) if short_config is None: return None @@ -257,7 +269,9 @@ def load_config_model(self, name, user, parameter_values=None, skip_invalid_para user, parameter_values, skip_invalid_parameters, - self._process_invoker) + self._process_invoker, + self._group_scripts_by_folder, + self._script_configs_folder) def _visit_script_configs(self, visitor): configs_dir = self._script_configs_folder @@ -296,7 +310,7 @@ def _find_config(self, name, user) -> Optional[ConfigSearchResult]: def find_and_load(path: str, content): try: config_object = self.load_config_file(path, content) - short_config = script_config.read_short(path, config_object) + short_config = self.read_short_config(config_object, path) if short_config is None: return None @@ -331,7 +345,9 @@ def _load_script_config( user, parameter_values, skip_invalid_parameters, - process_invoker): + process_invoker, + group_scripts_by_folder, + script_configs_folder): if isinstance(content_or_json_dict, str): json_object = custom_json.loads(content_or_json_dict) @@ -342,6 +358,8 @@ def _load_script_config( path, user.get_username(), user.get_audit_name(), + group_scripts_by_folder, + script_configs_folder, process_invoker, pty_enabled_default=os_utils.is_pty_supported()) diff --git a/src/main.py b/src/main.py index 6d6eaee7..e1054034 100644 --- a/src/main.py +++ b/src/main.py @@ -103,7 +103,8 @@ def main(): process_invoker = ProcessInvoker(server_config.env_vars) - config_service = ConfigService(authorizer, CONFIG_FOLDER, process_invoker) + config_service = ConfigService( + authorizer, CONFIG_FOLDER, server_config.groups_config.group_by_folders, process_invoker) alerts_service = AlertsService(server_config.alerts_config) alerts_service = alerts_service diff --git a/src/model/script_config.py b/src/model/script_config.py index 8475654a..77787204 100644 --- a/src/model/script_config.py +++ b/src/model/script_config.py @@ -62,14 +62,16 @@ def __init__(self, path, username, audit_name, + group_by_folders: bool, + script_configs_folder: str, process_invoker: ProcessInvoker, pty_enabled_default=True): super().__init__() - short_config = read_short(path, config_object) + short_config = read_short(path, config_object, group_by_folders, script_configs_folder) self.name = short_config.name self._pty_enabled_default = pty_enabled_default - self._config_folder = os.path.dirname(path) + self._config_folder = script_configs_folder self._process_invoker = process_invoker self._username = username @@ -363,11 +365,16 @@ def _build_name_from_path(file_path): return name.strip() -def read_short(file_path, json_object): +def read_short(file_path, json_object, group_by_folders: bool, script_configs_folder: str): name = _read_name(file_path, json_object) allowed_users = json_object.get('allowed_users') admin_users = json_object.get('admin_users') group = read_str_from_config(json_object, 'group', blank_to_none=True) + if not group and group_by_folders: + relative_path = file_utils.relative_path(file_path, script_configs_folder) + while os.path.dirname(relative_path): + relative_path = os.path.dirname(relative_path) + group = relative_path hidden = read_bool_from_config('hidden', json_object, default=False) if hidden: diff --git a/src/model/server_conf.py b/src/model/server_conf.py index 23bebd62..b8175486 100644 --- a/src/model/server_conf.py +++ b/src/model/server_conf.py @@ -29,6 +29,7 @@ def __init__(self) -> None: self.allowed_users = None self.alerts_config = None self.logging_config = None + self.groups_config = ScriptGroupsConfig() # type: ScriptGroupsConfig self.admin_config = None self.title = None self.enable_script_titles = None @@ -75,6 +76,24 @@ def from_json(cls, json_config): return config +class ScriptGroupsConfig: + + def __init__(self) -> None: + self.group_by_folders = True + + @classmethod + def from_json(cls, json_config): + config = ScriptGroupsConfig() + + if json_config: + config.group_by_folders = model_helper.read_bool_from_config( + 'group_by_folders', + json_config, + default=config.group_by_folders) + + return config + + def _build_env_vars(json_object): sensitive_config_paths = [ ['auth', 'secret'], @@ -184,6 +203,7 @@ def from_json(conf_path, temp_folder): config.alerts_config = json_object.get('alerts') config.callbacks_config = json_object.get('callbacks') config.logging_config = LoggingConfig.from_json(json_object.get('logging')) + config.groups_config = ScriptGroupsConfig.from_json(json_object.get('script_groups')) config.user_groups = user_groups config.admin_users = admin_users config.full_history_users = full_history_users diff --git a/src/tests/config_service_test.py b/src/tests/config_service_test.py index a2753df0..5291cb7f 100644 --- a/src/tests/config_service_test.py +++ b/src/tests/config_service_test.py @@ -1,6 +1,7 @@ import json import os import sys +import tempfile import unittest from collections import OrderedDict from shutil import copyfile @@ -30,6 +31,14 @@ def test_list_configs_when_one(self): self.assertEqual(1, len(configs)) self.assertEqual('conf_x', configs[0].name) + def test_list_configs_when_one_and_symlink(self): + conf_path = os.path.join(test_utils.temp_folder, 'runners', 'sub', 'x.json') + with self._temporary_file_symlink(conf_path, {'name': 'test X'}): + configs = self.config_service.list_configs(self.user) + self.assertEqual(1, len(configs)) + self.assertEqual('test X', configs[0].name) + self.assertEqual('sub', configs[0].group) + def test_list_configs_when_multiple(self): _create_script_config_file('conf_x') _create_script_config_file('conf_y') @@ -40,9 +49,9 @@ def test_list_configs_when_multiple(self): self.assertCountEqual(['conf_x', 'conf_y', 'A B C'], conf_names) def test_list_configs_when_multiple_and_subfolders(self): - _create_script_config_file('conf_x', subfolder = 's1') - _create_script_config_file('conf_y', subfolder = 's2') - _create_script_config_file('ABC', subfolder = os.path.join('s1', 'inner')) + _create_script_config_file('conf_x', subfolder='s1') + _create_script_config_file('conf_y', subfolder='s2') + _create_script_config_file('ABC', subfolder=os.path.join('s1', 'inner')) configs = self.config_service.list_configs(self.user) conf_names = [config.name for config in configs] @@ -114,6 +123,36 @@ def test_load_config_with_slash_in_name(self): config = self.config_service.load_config_model('Name with slash /', self.user) self.assertEqual('Name with slash /', config.name) + def test_list_configs_when_multiple_subfolders_and_symlink(self): + def create_config_file(name, relative_path, group=None): + filename = os.path.basename(relative_path) + test_utils.write_script_config( + {'name': name, 'group': group}, + filename, + config_folder=os.path.join(test_utils.temp_folder, 'runners', os.path.dirname(relative_path))) + + subfolder = os.path.join(test_utils.temp_folder, 'runners', 'sub') + symlink_path = os.path.join(subfolder, 'x.json') + with self._temporary_file_symlink(symlink_path, {'name': 'test X'}): + create_config_file('conf Y', os.path.join('sub', 'y', 'conf_y.json')) + create_config_file('conf Z', os.path.join('sub', 'z', 'conf_z.json')) + create_config_file('conf A', 'conf_a.json') + create_config_file('conf B', os.path.join('b', 'conf_b.json')) + create_config_file('conf C', os.path.join('c', 'conf_c.json'), group='test group') + + configs = self.config_service.list_configs(self.user) + actual_name_group_map = {c.name: c.group for c in configs} + + self.assertEqual( + actual_name_group_map, + {'test X': 'sub', + 'conf Y': 'sub', + 'conf Z': 'sub', + 'conf A': None, + 'conf B': 'b', + 'conf C': 'test group'}, + ) + def tearDown(self): super().tearDown() test_utils.cleanup() @@ -125,7 +164,19 @@ def setUp(self): self.user = User('ConfigServiceTest', {AUTH_USERNAME: 'ConfigServiceTest'}) self.admin_user = User('admin_user', {AUTH_USERNAME: 'The Admin'}) authorizer = Authorizer(ANY_USER, ['admin_user'], [], [], EmptyGroupProvider()) - self.config_service = ConfigService(authorizer, test_utils.temp_folder, test_utils.process_invoker) + self.config_service = ConfigService(authorizer, test_utils.temp_folder, True, test_utils.process_invoker) + + @staticmethod + def _temporary_file_symlink(symlink_path, file_content: dict): + f = tempfile.NamedTemporaryFile() + + f.write(json.dumps(file_content).encode('utf-8')) + f.flush() + subdir = os.path.dirname(symlink_path) + os.makedirs(subdir) + os.symlink(f.name, symlink_path) + + return f class ConfigServiceAuthTest(unittest.TestCase): @@ -209,7 +260,11 @@ def setUp(self): authorizer = Authorizer([], ['adm_user'], [], [], EmptyGroupProvider()) self.user1 = User('user1', {}) self.admin_user = User('adm_user', {}) - self.config_service = ConfigService(authorizer, test_utils.temp_folder, test_utils.process_invoker) + self.config_service = ConfigService( + authorizer, + test_utils.temp_folder, + True, + test_utils.process_invoker) def script_path(path): @@ -242,7 +297,7 @@ def setUp(self): authorizer = Authorizer([], ['admin_user', 'admin_non_editor'], [], ['admin_user'], EmptyGroupProvider()) self.admin_user = User('admin_user', {}) - self.config_service = ConfigService(authorizer, test_utils.temp_folder, test_utils.process_invoker) + self.config_service = ConfigService(authorizer, test_utils.temp_folder, True, test_utils.process_invoker) def tearDown(self): super().tearDown() @@ -416,7 +471,7 @@ def setUp(self): authorizer = Authorizer([], ['admin_user', 'admin_non_editor'], [], ['admin_user'], EmptyGroupProvider()) self.admin_user = User('admin_user', {}) - self.config_service = ConfigService(authorizer, test_utils.temp_folder, test_utils.process_invoker) + self.config_service = ConfigService(authorizer, test_utils.temp_folder, True, test_utils.process_invoker) for suffix in 'XYZ': name = 'Conf ' + suffix @@ -669,7 +724,7 @@ def setUp(self): authorizer = Authorizer([], ['admin_user'], [], [], EmptyGroupProvider()) self.admin_user = User('admin_user', {}) - self.config_service = ConfigService(authorizer, test_utils.temp_folder, test_utils.process_invoker) + self.config_service = ConfigService(authorizer, test_utils.temp_folder, True, test_utils.process_invoker) def tearDown(self): super().tearDown() @@ -717,7 +772,7 @@ def setUp(self) -> None: authorizer = Authorizer([], ['admin_user', 'admin_non_editor'], [], ['admin_user'], EmptyGroupProvider()) self.admin_user = User('admin_user', {}) - self.config_service = ConfigService(authorizer, test_utils.temp_folder, test_utils.process_invoker) + self.config_service = ConfigService(authorizer, test_utils.temp_folder, True, test_utils.process_invoker) for pair in [('script.py', b'123'), ('another.py', b'xyz'), diff --git a/src/tests/execution_logging_test.py b/src/tests/execution_logging_test.py index 738b4da9..d057d6e1 100644 --- a/src/tests/execution_logging_test.py +++ b/src/tests/execution_logging_test.py @@ -542,7 +542,8 @@ def test_logging_values(self): 'my_script', script_command='echo', parameters=[param1, param2, param3, param4], - logging_config=LoggingConfig('test-${SCRIPT}-${p1}')) + logging_config=LoggingConfig('test-${SCRIPT}-${p1}'), + path=os.path.join('conf', 'my_script.json')) config_model.set_all_param_values({'p1': 'abc', 'p3': True, 'p4': 987}) execution_id = self.executor_service.start_script( @@ -568,7 +569,7 @@ def test_logging_values(self): self.assertEqual('some text\nanother text', log) log_files = os.listdir(test_utils.temp_folder) - self.assertEqual(['test-my_script-abc.log'], log_files) + self.assertEqual(['test-my_script-abc.log', 'conf'], log_files) def test_exit_code(self): config_model = create_config_model( diff --git a/src/tests/execution_service_test.py b/src/tests/execution_service_test.py index 0e5346c2..9ffa4c23 100644 --- a/src/tests/execution_service_test.py +++ b/src/tests/execution_service_test.py @@ -10,7 +10,6 @@ from execution.execution_service import ExecutionService from execution.executor import create_process_wrapper from model.model_helper import AccessProhibitedException -from model.script_config import ConfigModel from tests import test_utils from tests.test_utils import mock_object, create_audit_names, _MockProcessWrapper, _IdGeneratorMock from utils import audit_utils @@ -441,10 +440,12 @@ def _start_with_config(execution_service, config, parameter_values=None, user_id def _create_script_config(parameter_configs): - config = ConfigModel( - {'name': 'script_x', - 'script_path': 'ls', - 'parameters': parameter_configs}, - 'script_x.json', 'user1', 'localhost', - test_utils.process_invoker) - return config + return test_utils.create_config_model( + 'script_x', + config={'name': 'script_x', + 'script_path': 'ls', + 'parameters': parameter_configs}, + username='user1', + audit_name='localhost', + + ) diff --git a/src/tests/scheduling/schedule_service_test.py b/src/tests/scheduling/schedule_service_test.py index 4f944d2a..ef32a423 100644 --- a/src/tests/scheduling/schedule_service_test.py +++ b/src/tests/scheduling/schedule_service_test.py @@ -60,7 +60,11 @@ def setUp(self) -> None: scheduler._sleep = MagicMock() scheduler._sleep.side_effect = lambda x: time.sleep(0.001) - self.config_service = ConfigService(AnyUserAuthorizer(), test_utils.temp_folder, test_utils.process_invoker) + self.config_service = ConfigService( + AnyUserAuthorizer(), + test_utils.temp_folder, + True, + test_utils.process_invoker) self.create_config('my_script_A') self.create_config('unschedulable-script', scheduling_enabled=False) diff --git a/src/tests/script_config_test.py b/src/tests/script_config_test.py index 1acfa8c6..cfd0c8c8 100644 --- a/src/tests/script_config_test.py +++ b/src/tests/script_config_test.py @@ -6,7 +6,7 @@ from config.constants import PARAM_TYPE_SERVER_FILE, PARAM_TYPE_MULTISELECT from config.exceptions import InvalidConfigException -from model.script_config import ConfigModel, InvalidValueException, TemplateProperty, ParameterNotFoundException, \ +from model.script_config import InvalidValueException, TemplateProperty, ParameterNotFoundException, \ get_sorted_config from model.value_wrapper import ScriptValueWrapper from react.properties import ObservableDict, ObservableList @@ -259,10 +259,10 @@ def test_list_files_for_valid_param(self): param = create_script_param_config('recurs_file', type=PARAM_TYPE_SERVER_FILE, file_recursive=True, - file_dir=test_utils.temp_folder) + file_dir=self.subfolder) config_model = _create_config_model('my_conf', parameters=[param]) - create_files(['file1', 'file2']) + create_files(['file1', 'file2'], 'sub') file_names = [f['name'] for f in (config_model.list_files_for_param('recurs_file', []))] self.assertCountEqual(['file1', 'file2'], file_names) @@ -271,14 +271,14 @@ def test_list_files_when_working_dir(self): type=PARAM_TYPE_SERVER_FILE, file_recursive=True, file_dir='.') - config_model = _create_config_model('my_conf', parameters=[param], working_dir=test_utils.temp_folder) + config_model = _create_config_model('my_conf', parameters=[param], working_dir=self.subfolder) - create_files(['file1', 'file2']) + create_files(['file1', 'file2'], 'sub') file_names = [f['name'] for f in (config_model.list_files_for_param('recurs_file', []))] self.assertCountEqual(['file1', 'file2'], file_names) def test_list_files_when_unknown_param(self): - config_model = _create_config_model('my_conf', parameters=[], working_dir=test_utils.temp_folder) + config_model = _create_config_model('my_conf', parameters=[], working_dir=self.subfolder) self.assertRaises(ParameterNotFoundException, config_model.list_files_for_param, 'recurs_file', []) @@ -286,6 +286,9 @@ def setUp(self): super().setUp() test_utils.setup() + self.subfolder = os.path.join(test_utils.temp_folder, 'sub') + os.mkdir(self.subfolder) + def tearDown(self): super().tearDown() test_utils.cleanup() @@ -399,14 +402,17 @@ def test_get_required_parameters(self): class ConfigModelIncludeTest(unittest.TestCase): def test_static_include_simple(self): included_path = test_utils.write_script_config({'script_path': 'ping google.com'}, 'included') + included_path = file_utils.relative_path(included_path, test_utils.temp_folder) config_model = _create_config_model('main_conf', script_path=None, config={'include': included_path}) self.assertEqual('ping google.com', config_model.script_command) def test_static_include_multiple_inclusions(self): included_path_1 = test_utils.write_script_config({'script_path': 'ping google.com'}, 'included1') + included_path_1 = file_utils.relative_path(included_path_1, test_utils.temp_folder) included_path_2 = test_utils.write_script_config( {'script_path': 'echo 123', 'working_directory': '123'}, 'included2') + included_path_2 = file_utils.relative_path(included_path_2, test_utils.temp_folder) config_model = _create_config_model( 'main_conf', script_path=None, @@ -419,6 +425,7 @@ def test_static_include_precedence(self): 'script_path': 'ping google.com', 'working_directory': '123'}, 'included') + included_path = file_utils.relative_path(included_path, test_utils.temp_folder) config_model = _create_config_model('main_conf', config={ 'include': included_path, 'working_directory': 'abc'}) @@ -429,6 +436,7 @@ def test_static_include_single_parameter(self): included_path = test_utils.write_script_config({'parameters': [ create_script_param_config('param2', type='int') ]}, 'included1') + included_path = file_utils.relative_path(included_path, test_utils.temp_folder) config_model = _create_config_model('main_conf', config={ 'include': included_path, 'parameters': [create_script_param_config('param1', type='text')]}) @@ -448,12 +456,14 @@ def test_static_include_multiple_parameters_from_multiple_included(self): create_script_param_config('param2', type='int'), create_script_param_config('param3'), ]}, 'included1') + included_path_1 = file_utils.relative_path(included_path_1, test_utils.temp_folder) included_path_2 = test_utils.write_script_config({ 'parameters': [ create_script_param_config('param2', type='ip4'), create_script_param_config('param4'), create_script_param_config(None), ]}, 'included2') + included_path_2 = file_utils.relative_path(included_path_2, test_utils.temp_folder) config_model = _create_config_model('main_conf', config={ 'include': [included_path_1, included_path_2], @@ -482,6 +492,7 @@ def test_static_include_hidden_config(self): 'script_path': 'ping google.com', 'hidden': True}, 'included') + included_path = file_utils.relative_path(included_path, test_utils.temp_folder) config_model = _create_config_model('main_conf', script_path=None, config={'include': included_path}) self.assertEqual('ping google.com', config_model.script_command) @@ -546,6 +557,7 @@ def test_dynamic_include_relative_path(self): included_path = test_utils.write_script_config({'parameters': [ create_script_param_config('included_param') ]}, 'included', folder) + included_path = file_utils.relative_path(included_path, test_utils.temp_folder) included_folder = os.path.dirname(included_path) config_model = _create_config_model( 'main_conf', @@ -554,7 +566,7 @@ def test_dynamic_include_relative_path(self): 'include': '${p1}', 'working_directory': included_folder, 'parameters': [create_script_param_config('p1')]}) - config_model.set_param_value('p1', 'included.json') + config_model.set_param_value('p1', included_path) self.assertEqual(2, len(config_model.parameters)) @@ -566,6 +578,7 @@ def test_dynamic_include_replace(self): included_path2 = test_utils.write_script_config({'parameters': [ create_script_param_config('included_param_Y') ]}, 'included2') + included_path2 = file_utils.relative_path(included_path2, test_utils.temp_folder) config_model.set_param_value('p1', included_path2) @@ -588,6 +601,7 @@ def test_set_all_values_for_included(self): create_script_param_config('included_param1'), create_script_param_config('included_param2') ]}, 'included') + included_path = file_utils.relative_path(included_path, test_utils.temp_folder) config_model = _create_config_model( 'main_conf', config={ @@ -604,6 +618,7 @@ def test_set_all_values_for_dependant_on_constant(self): included_path = test_utils.write_script_config({'parameters': [ create_script_param_config('included_param1', values_script='echo ${p1}'), ]}, 'included') + included_path = file_utils.relative_path(included_path, test_utils.temp_folder) config_model = _create_config_model( 'main_conf', config={ @@ -659,12 +674,14 @@ def test_dynamic_include_when_multiple_includes(self, param2_value, expected_des 'parameters': [ create_script_param_config('param3', type='int'), ]}, 'included1') + included_path_1 = file_utils.relative_path(included_path_1, test_utils.temp_folder) included_path_2 = test_utils.write_script_config({ 'description': 'test desc', 'parameters': [ create_script_param_config('param3', type='ip4'), create_script_param_config('param4') ]}, 'included2') + included_path_2 = file_utils.relative_path(included_path_2, test_utils.temp_folder) config_model = _create_config_model('main_conf', config={ 'include': ['${param1}', included_path_2[:-6] + '${param2}.json'], @@ -686,6 +703,7 @@ def test_dynamic_include_when_multiple_includes(self, param2_value, expected_des def prepare_config_model_with_included(self, included_params, static_param_name): included_path = test_utils.write_script_config({'parameters': included_params}, 'included') + included_path = file_utils.relative_path(included_path, test_utils.temp_folder) config_model = _create_config_model('main_conf', config={ 'include': '${' + static_param_name + '}', 'parameters': [create_script_param_config(static_param_name)]}) @@ -1079,6 +1097,7 @@ def test_create_with_schedulable_true_and_included_secure_parameter(self): another_path = test_utils.write_script_config( {'parameters': [{'name': 'p2', 'secure': True}]}, 'another_config') + another_path = file_utils.relative_path(another_path, test_utils.temp_folder) self.assertTrue(config_model.schedulable) @@ -1177,28 +1196,31 @@ def _create_config_model(name, *, parameters=None, parameter_values=None, working_dir=None, - script_path='echo 123', + script_path='DEFAULT', skip_invalid_parameters=False): result_config = {} - if script_path is not None: - result_config['script_path'] = script_path - if config: result_config.update(config) - result_config['name'] = name - - if parameters is not None: - result_config['parameters'] = parameters - - if path is None: - path = name - if working_dir is not None: result_config['working_directory'] = working_dir - model = ConfigModel(result_config, path, username, audit_name, test_utils.process_invoker) + if script_path == 'DEFAULT': + if config and 'script_path' in config: + script_path = None + else: + script_path = 'echo 123' + + model = test_utils.create_config_model( + name, + script_command=script_path, + config=result_config, + username=username, + audit_name=audit_name, + path=path, + parameters=parameters) + if parameter_values is not None: model.set_all_param_values(parameter_values, skip_invalid_parameters=skip_invalid_parameters) diff --git a/src/tests/test_utils.py b/src/tests/test_utils.py index 45c6ca15..7ec447e3 100644 --- a/src/tests/test_utils.py +++ b/src/tests/test_utils.py @@ -121,10 +121,15 @@ def mock_object(): def write_script_config(conf_object, filename, config_folder=None): if config_folder is None: config_folder = os.path.join(temp_folder, 'runners') - file_path = os.path.join(config_folder, filename + '.json') + + if not filename.endswith('.json'): + filename = filename + '.json' + + file_path = os.path.join(config_folder, filename) config_json = json.dumps(conf_object) file_utils.write_file(file_path, config_json) + return file_path @@ -222,7 +227,7 @@ def create_config_model(name, *, script_command='ls', output_files=None, requires_terminal=None, - schedulable=True, + schedulable=None, logging_config: LoggingConfig = None, output_format=None): result_config = {} @@ -236,7 +241,9 @@ def create_config_model(name, *, result_config['parameters'] = parameters if path is None: - path = name + path = create_file(name + '.json', text='{}', overwrite=True) + elif not os.path.exists(path): + path = create_file(path, text='{}', overwrite=True) if output_files is not None: result_config['output_files'] = output_files @@ -245,7 +252,10 @@ def create_config_model(name, *, result_config['requires_terminal'] = requires_terminal if schedulable is not None: - result_config['scheduling'] = {'enabled': schedulable} + if 'scheduling' in result_config: + result_config['scheduling']['enabled'] = schedulable + else: + result_config['scheduling'] = {'enabled': schedulable} if output_format: result_config['output_format'] = output_format @@ -255,9 +265,17 @@ def create_config_model(name, *, 'execution_file': logging_config.filename_pattern, 'execution_date_format': logging_config.date_format} - result_config['script_path'] = script_command + if script_command: + result_config['script_path'] = script_command - model = ConfigModel(result_config, path, username, audit_name, process_invoker) + model = ConfigModel( + result_config, + path, + username, + audit_name, + True, + temp_folder, + process_invoker) if parameter_values is not None: model.set_all_param_values(parameter_values) diff --git a/src/tests/web/script_config_socket_test.py b/src/tests/web/script_config_socket_test.py index 74a4be46..176429d8 100644 --- a/src/tests/web/script_config_socket_test.py +++ b/src/tests/web/script_config_socket_test.py @@ -234,7 +234,7 @@ def setUp(self): application.authorizer = Authorizer(ANY_USER, [], [], [], EmptyGroupProvider()) application.identification = IpBasedIdentification(TrustedIpValidator(['127.0.0.1']), None) application.config_service = ConfigService( - application.authorizer, test_utils.temp_folder, test_utils.process_invoker) + application.authorizer, test_utils.temp_folder, True, test_utils.process_invoker) server = httpserver.HTTPServer(application) socket, self.port = testing.bind_unused_port() diff --git a/src/tests/web/server_test.py b/src/tests/web/server_test.py index 7d19a3e8..7a4abbbe 100644 --- a/src/tests/web/server_test.py +++ b/src/tests/web/server_test.py @@ -295,7 +295,7 @@ def start_server(self, port, address, *, xsrf_protection=XSRF_PROTECTION_TOKEN): execution_service, MagicMock(), MagicMock(), - ConfigService(authorizer, self.conf_folder, test_utils.process_invoker), + ConfigService(authorizer, self.conf_folder, True, test_utils.process_invoker), MagicMock(), FileUploadFeature(UserFileStorage(cookie_secret), test_utils.temp_folder), file_download_feature, diff --git a/src/utils/file_utils.py b/src/utils/file_utils.py index c2b87ab4..3004b6cf 100644 --- a/src/utils/file_utils.py +++ b/src/utils/file_utils.py @@ -33,7 +33,7 @@ def is_root(path): return os.path.dirname(path) == path -def normalize_path(path_string, current_folder=None): +def normalize_path(path_string, current_folder=None, follow_symlinks=True): path_string = os.path.expanduser(path_string) path_string = os.path.normpath(path_string) @@ -41,13 +41,16 @@ def normalize_path(path_string, current_folder=None): return path_string if current_folder: - normalized_folder = normalize_path(current_folder) + normalized_folder = normalize_path(current_folder, follow_symlinks=follow_symlinks) return os.path.join(normalized_folder, path_string) if not os.path.exists(path_string): return path_string - return str(pathlib.Path(path_string).resolve()) + if follow_symlinks: + return str(pathlib.Path(path_string).resolve()) + else: + return str(pathlib.Path(path_string).absolute()) def read_file(filename, byte_content=False, keep_newlines=False): @@ -143,17 +146,22 @@ def last_modification(folder_paths): def relative_path(path, parent_path): - path = normalize_path(path) - parent_path = normalize_path(parent_path) - - if os_utils.is_win(): - path = path.capitalize() - parent_path = parent_path.capitalize() - - if not path.startswith(parent_path): - raise ValueError(path + ' is not subpath of ' + parent_path) - - relative_path = path[len(parent_path):] + def normalize(path, follow_symlinks=True): + path = normalize_path(path, follow_symlinks=follow_symlinks) + if os_utils.is_win(): + path = path.capitalize() + return path + + normalized_path = normalize(path) + normalized_parent_path = normalize(parent_path) + + if not normalized_path.startswith(normalized_parent_path): + normalized_path = normalize(path, follow_symlinks=False) + normalized_parent_path = normalize(parent_path, follow_symlinks=False) + if not normalized_path.startswith(normalized_parent_path): + raise ValueError(path + ' is not subpath of ' + parent_path) + + relative_path = normalized_path[len(normalized_parent_path):] if relative_path.startswith(os.path.sep): return relative_path[1:] From 44bd5f2cde91f82bee0b881c0c926276ab24551c Mon Sep 17 00:00:00 2001 From: yshepilov Date: Sun, 15 Oct 2023 17:57:39 +0200 Subject: [PATCH 005/148] #699 added automatic script groups based on sub-folders --- src/model/script_config.py | 2 +- src/tests/config_service_test.py | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/model/script_config.py b/src/model/script_config.py index 77787204..1574f85e 100644 --- a/src/model/script_config.py +++ b/src/model/script_config.py @@ -370,7 +370,7 @@ def read_short(file_path, json_object, group_by_folders: bool, script_configs_fo allowed_users = json_object.get('allowed_users') admin_users = json_object.get('admin_users') group = read_str_from_config(json_object, 'group', blank_to_none=True) - if not group and group_by_folders: + if ('group' not in json_object) and group_by_folders: relative_path = file_utils.relative_path(file_path, script_configs_folder) while os.path.dirname(relative_path): relative_path = os.path.dirname(relative_path) diff --git a/src/tests/config_service_test.py b/src/tests/config_service_test.py index 5291cb7f..c1430bfd 100644 --- a/src/tests/config_service_test.py +++ b/src/tests/config_service_test.py @@ -126,8 +126,11 @@ def test_load_config_with_slash_in_name(self): def test_list_configs_when_multiple_subfolders_and_symlink(self): def create_config_file(name, relative_path, group=None): filename = os.path.basename(relative_path) + config = {'name': name} + if group is not None: + config['group'] = group test_utils.write_script_config( - {'name': name, 'group': group}, + config, filename, config_folder=os.path.join(test_utils.temp_folder, 'runners', os.path.dirname(relative_path))) @@ -139,6 +142,7 @@ def create_config_file(name, relative_path, group=None): create_config_file('conf A', 'conf_a.json') create_config_file('conf B', os.path.join('b', 'conf_b.json')) create_config_file('conf C', os.path.join('c', 'conf_c.json'), group='test group') + create_config_file('conf D', os.path.join('d', 'conf_d.json'), group='') configs = self.config_service.list_configs(self.user) actual_name_group_map = {c.name: c.group for c in configs} @@ -150,7 +154,8 @@ def create_config_file(name, relative_path, group=None): 'conf Z': 'sub', 'conf A': None, 'conf B': 'b', - 'conf C': 'test group'}, + 'conf C': 'test group', + 'conf D': None}, ) def tearDown(self): From 10f26e2fe1eb7e3cb5edf4869cc8c456ad8e587b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Oct 2023 01:40:19 +0000 Subject: [PATCH 006/148] Bump @babel/traverse from 7.21.4 to 7.23.2 in /web-src Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.21.4 to 7.23.2. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse) --- updated-dependencies: - dependency-name: "@babel/traverse" dependency-type: indirect ... Signed-off-by: dependabot[bot] --- web-src/package-lock.json | 314 +++++++++++++++++++++++++------------- 1 file changed, 204 insertions(+), 110 deletions(-) diff --git a/web-src/package-lock.json b/web-src/package-lock.json index 669f389c..bdef78f9 100644 --- a/web-src/package-lock.json +++ b/web-src/package-lock.json @@ -181,12 +181,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", - "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dev": true, "dependencies": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" @@ -360,9 +361,9 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, "engines": { "node": ">=6.9.0" @@ -381,25 +382,25 @@ } }, "node_modules/@babel/helper-function-name": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", - "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "dependencies": { - "@babel/template": "^7.20.7", - "@babel/types": "^7.21.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -529,30 +530,30 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true, "engines": { "node": ">=6.9.0" @@ -597,13 +598,13 @@ } }, "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -2011,33 +2012,45 @@ } }, "node_modules/@babel/template": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", - "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.4.tgz", - "integrity": "sha512-eyKrRHKdyZxqDm+fV1iqL9UAHMoIg0nDaGqfIOd8rKH17m5snv7Gn4qgjBoFfLz9APvjFU/ICT00NVCv1Epp8Q==", + "node_modules/@babel/template/node_modules/@babel/parser": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "dev": true, - "dependencies": { - "@babel/code-frame": "^7.21.4", - "@babel/generator": "^7.21.4", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.21.4", - "@babel/types": "^7.21.4", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -2045,14 +2058,55 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/parser": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/traverse/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/types": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", - "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -25158,12 +25212,13 @@ } }, "@babel/code-frame": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", - "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dev": true, "requires": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" } }, "@babel/compat-data": { @@ -25293,9 +25348,9 @@ } }, "@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true }, "@babel/helper-explode-assignable-expression": { @@ -25308,22 +25363,22 @@ } }, "@babel/helper-function-name": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", - "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "requires": { - "@babel/template": "^7.20.7", - "@babel/types": "^7.21.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" } }, "@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-member-expression-to-functions": { @@ -25420,24 +25475,24 @@ } }, "@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true }, "@babel/helper-validator-option": { @@ -25470,13 +25525,13 @@ } }, "@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" } }, @@ -26421,42 +26476,81 @@ } }, "@babel/template": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", - "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + }, + "dependencies": { + "@babel/parser": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "dev": true + } } }, "@babel/traverse": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.4.tgz", - "integrity": "sha512-eyKrRHKdyZxqDm+fV1iqL9UAHMoIg0nDaGqfIOd8rKH17m5snv7Gn4qgjBoFfLz9APvjFU/ICT00NVCv1Epp8Q==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.21.4", - "@babel/generator": "^7.21.4", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.21.4", - "@babel/types": "^7.21.4", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" + }, + "dependencies": { + "@babel/generator": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "dev": true, + "requires": { + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + } + }, + "@babel/parser": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "dev": true + }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + } } }, "@babel/types": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", - "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } }, From 311e3681a6e23487cd128445b93740b5012ae05a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 28 Oct 2023 00:54:11 +0000 Subject: [PATCH 007/148] Bump browserify-sign from 4.2.1 to 4.2.2 in /web-src Bumps [browserify-sign](https://github.com/crypto-browserify/browserify-sign) from 4.2.1 to 4.2.2. - [Changelog](https://github.com/browserify/browserify-sign/blob/main/CHANGELOG.md) - [Commits](https://github.com/crypto-browserify/browserify-sign/compare/v4.2.1...v4.2.2) --- updated-dependencies: - dependency-name: browserify-sign dependency-type: indirect ... Signed-off-by: dependabot[bot] --- web-src/package-lock.json | 51 +++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/web-src/package-lock.json b/web-src/package-lock.json index bdef78f9..26ded486 100644 --- a/web-src/package-lock.json +++ b/web-src/package-lock.json @@ -6674,26 +6674,29 @@ } }, "node_modules/browserify-sign": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", - "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.2.tgz", + "integrity": "sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg==", "dev": true, "dependencies": { - "bn.js": "^5.1.1", - "browserify-rsa": "^4.0.1", + "bn.js": "^5.2.1", + "browserify-rsa": "^4.1.0", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", - "elliptic": "^6.5.3", + "elliptic": "^6.5.4", "inherits": "^2.0.4", - "parse-asn1": "^5.1.5", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" + "parse-asn1": "^5.1.6", + "readable-stream": "^3.6.2", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 4" } }, "node_modules/browserify-sign/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, "dependencies": { "inherits": "^2.0.3", @@ -30280,26 +30283,26 @@ } }, "browserify-sign": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", - "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.2.tgz", + "integrity": "sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg==", "dev": true, "requires": { - "bn.js": "^5.1.1", - "browserify-rsa": "^4.0.1", + "bn.js": "^5.2.1", + "browserify-rsa": "^4.1.0", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", - "elliptic": "^6.5.3", + "elliptic": "^6.5.4", "inherits": "^2.0.4", - "parse-asn1": "^5.1.5", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" + "parse-asn1": "^5.1.6", + "readable-stream": "^3.6.2", + "safe-buffer": "^5.2.1" }, "dependencies": { "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, "requires": { "inherits": "^2.0.3", From 550c9f654ae82c201b01ef1f63011d91fe19cb1e Mon Sep 17 00:00:00 2001 From: Vo Van Nghia Date: Tue, 7 Nov 2023 12:11:55 +0100 Subject: [PATCH 008/148] enable multiarch image --- tools/deploy_docker.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/deploy_docker.sh b/tools/deploy_docker.sh index 5ac6586e..fed4828d 100755 --- a/tools/deploy_docker.sh +++ b/tools/deploy_docker.sh @@ -19,9 +19,11 @@ else DOCKER_TAG="$TRAVIS_BRANCH" fi +docker run --rm --privileged multiarch/qemu-user-static --reset -p yes + docker login -u "$DOCKER_USER" -p "$DOCKER_PASSWORD" -docker build -f tools/Dockerfile -t "$IMAGE_NAME":"$DOCKER_TAG" . +docker buildx build --platform linux/amd64,linux/arm64 -f tools/Dockerfile -t "$IMAGE_NAME":"$DOCKER_TAG" . echo "NEW_GIT_TAG=$NEW_GIT_TAG" if [ ! -z "$NEW_GIT_TAG" ]; then From b558807fb7943074338cf7b53be4599af6b2c6a6 Mon Sep 17 00:00:00 2001 From: Vo Van Nghia Date: Tue, 7 Nov 2023 18:17:43 +0100 Subject: [PATCH 009/148] install docker buildx --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index cfb1fcfd..b63bb245 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,6 +31,9 @@ before_install: - sudo apt-get -y install python3-pip python3-setuptools apache2-utils python3-venv - wget https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/116.0.5845.96/linux64/chromedriver-linux64.zip - unzip chromedriver-linux64.zip -d $HOME/.local/bin + - mkdir -vp ~/.docker/cli-plugins/ + - curl --silent -L "https://github.com/docker/buildx/releases/download/v0.11.2/buildx-v0.11.2.linux-amd64" > ~/.docker/cli-plugins/docker-buildx + - chmod a+x ~/.docker/cli-plugins/docker-buildx install: - pip3 install -r requirements.txt - pip3 install pyasn1 --upgrade From bbc50d5e22d1c1aed86961ae69962e9137607d56 Mon Sep 17 00:00:00 2001 From: Vo Van Nghia Date: Wed, 8 Nov 2023 08:38:18 +0100 Subject: [PATCH 010/148] create new docker builder --- tools/deploy_docker.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/deploy_docker.sh b/tools/deploy_docker.sh index fed4828d..73306b4a 100755 --- a/tools/deploy_docker.sh +++ b/tools/deploy_docker.sh @@ -23,11 +23,11 @@ docker run --rm --privileged multiarch/qemu-user-static --reset -p yes docker login -u "$DOCKER_USER" -p "$DOCKER_PASSWORD" -docker buildx build --platform linux/amd64,linux/arm64 -f tools/Dockerfile -t "$IMAGE_NAME":"$DOCKER_TAG" . +docker buildx create --use +docker buildx build --platform linux/amd64,linux/arm64 --push -f tools/Dockerfile -t "$IMAGE_NAME":"$DOCKER_TAG" . echo "NEW_GIT_TAG=$NEW_GIT_TAG" if [ ! -z "$NEW_GIT_TAG" ]; then docker tag "$IMAGE_NAME":"$DOCKER_TAG" "$IMAGE_NAME":"$NEW_GIT_TAG" + docker push "$IMAGE_NAME":"$NEW_GIT_TAG" fi - -docker push --all-tags "$IMAGE_NAME" From bdea0f01e8c139b929400263774d6fe8347beada Mon Sep 17 00:00:00 2001 From: yshepilov Date: Wed, 8 Nov 2023 09:10:43 +0100 Subject: [PATCH 011/148] fix multi-tag docker image build --- tools/deploy_docker.sh | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tools/deploy_docker.sh b/tools/deploy_docker.sh index 73306b4a..9c7c582c 100755 --- a/tools/deploy_docker.sh +++ b/tools/deploy_docker.sh @@ -23,11 +23,13 @@ docker run --rm --privileged multiarch/qemu-user-static --reset -p yes docker login -u "$DOCKER_USER" -p "$DOCKER_PASSWORD" -docker buildx create --use -docker buildx build --platform linux/amd64,linux/arm64 --push -f tools/Dockerfile -t "$IMAGE_NAME":"$DOCKER_TAG" . - -echo "NEW_GIT_TAG=$NEW_GIT_TAG" +ADDITIONAL_TAG_ARG="" if [ ! -z "$NEW_GIT_TAG" ]; then - docker tag "$IMAGE_NAME":"$DOCKER_TAG" "$IMAGE_NAME":"$NEW_GIT_TAG" - docker push "$IMAGE_NAME":"$NEW_GIT_TAG" + ADDITIONAL_TAG_ARG="-t '$IMAGE_NAME:$NEW_GIT_TAG'" fi + +docker buildx create --use +docker buildx build --platform linux/amd64,linux/arm64 --push -f tools/Dockerfile \ + -t "$IMAGE_NAME":"$DOCKER_TAG" \ + $ADDITIONAL_TAG_ARG \ + . From d47b6bcea792d437a7a4d0fca2b9d57c8bfae883 Mon Sep 17 00:00:00 2001 From: yshepilov Date: Wed, 8 Nov 2023 10:01:32 +0100 Subject: [PATCH 012/148] fix multi-tag docker image build --- tools/deploy_docker.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/deploy_docker.sh b/tools/deploy_docker.sh index 9c7c582c..c9bba19d 100755 --- a/tools/deploy_docker.sh +++ b/tools/deploy_docker.sh @@ -25,7 +25,7 @@ docker login -u "$DOCKER_USER" -p "$DOCKER_PASSWORD" ADDITIONAL_TAG_ARG="" if [ ! -z "$NEW_GIT_TAG" ]; then - ADDITIONAL_TAG_ARG="-t '$IMAGE_NAME:$NEW_GIT_TAG'" + ADDITIONAL_TAG_ARG="-t $IMAGE_NAME:$NEW_GIT_TAG" fi docker buildx create --use From 3550ae4a037cd47a249b9a6896d1ab0f058e7f1e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 11 Nov 2023 05:54:05 +0000 Subject: [PATCH 013/148] Bump axios from 0.27.2 to 1.6.0 in /web-src Bumps [axios](https://github.com/axios/axios) from 0.27.2 to 1.6.0. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v0.27.2...v1.6.0) --- updated-dependencies: - dependency-name: axios dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- web-src/package-lock.json | 34 +++++++++++++++++++++++----------- web-src/package.json | 2 +- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/web-src/package-lock.json b/web-src/package-lock.json index 26ded486..e7354e3a 100644 --- a/web-src/package-lock.json +++ b/web-src/package-lock.json @@ -9,7 +9,7 @@ "version": "1.18.0", "dependencies": { "ace-builds": "^1.11.2", - "axios": "^0.27.2", + "axios": "^1.6.0", "brace": "^0.11.1", "codemirror": "^5.65.9", "core-js": "^3.25.3", @@ -6114,12 +6114,13 @@ "dev": true }, "node_modules/axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz", + "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==", "dependencies": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "node_modules/axios-mock-adapter": { @@ -18180,6 +18181,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", @@ -29812,12 +29818,13 @@ "dev": true }, "axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz", + "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==", "requires": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" }, "dependencies": { "form-data": { @@ -39456,6 +39463,11 @@ "ipaddr.js": "1.9.1" } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", diff --git a/web-src/package.json b/web-src/package.json index 62cbb500..7d54c86a 100644 --- a/web-src/package.json +++ b/web-src/package.json @@ -4,7 +4,7 @@ "private": true, "dependencies": { "ace-builds": "^1.11.2", - "axios": "^0.27.2", + "axios": "^1.6.0", "brace": "^0.11.1", "codemirror": "^5.65.9", "core-js": "^3.25.3", From e44f10ce6f971d6a85f97c992afa6d04548fcda4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Nov 2023 11:06:29 +0000 Subject: [PATCH 014/148] Bump the npm_and_yarn at /web-src security update group Bumps the npm_and_yarn at /web-src security update group in /web-src with 1 update: [rss-parser](https://github.com/bobby-brennan/rss-parser). - [Commits](https://github.com/bobby-brennan/rss-parser/compare/v3.12.0...v3.13.0) --- updated-dependencies: - dependency-name: rss-parser dependency-type: indirect ... Signed-off-by: dependabot[bot] --- web-src/package-lock.json | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/web-src/package-lock.json b/web-src/package-lock.json index e7354e3a..0dcf8ecb 100644 --- a/web-src/package-lock.json +++ b/web-src/package-lock.json @@ -18968,13 +18968,13 @@ } }, "node_modules/rss-parser": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/rss-parser/-/rss-parser-3.12.0.tgz", - "integrity": "sha512-aqD3E8iavcCdkhVxNDIdg1nkBI17jgqF+9OqPS1orwNaOgySdpvq6B+DoONLhzjzwV8mWg37sb60e4bmLK117A==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/rss-parser/-/rss-parser-3.13.0.tgz", + "integrity": "sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w==", "dev": true, "dependencies": { "entities": "^2.0.3", - "xml2js": "^0.4.19" + "xml2js": "^0.5.0" } }, "node_modules/run-async": { @@ -24850,9 +24850,9 @@ } }, "node_modules/xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", "dev": true, "dependencies": { "sax": ">=0.6.0", @@ -40092,13 +40092,13 @@ } }, "rss-parser": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/rss-parser/-/rss-parser-3.12.0.tgz", - "integrity": "sha512-aqD3E8iavcCdkhVxNDIdg1nkBI17jgqF+9OqPS1orwNaOgySdpvq6B+DoONLhzjzwV8mWg37sb60e4bmLK117A==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/rss-parser/-/rss-parser-3.13.0.tgz", + "integrity": "sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w==", "dev": true, "requires": { "entities": "^2.0.3", - "xml2js": "^0.4.19" + "xml2js": "^0.5.0" } }, "run-async": { @@ -44857,9 +44857,9 @@ "requires": {} }, "xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", "dev": true, "requires": { "sax": ">=0.6.0", From d587f0e25e1b34921adb64fa69c2c99f9dc270b4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Nov 2023 21:02:03 +0000 Subject: [PATCH 015/148] Bump @adobe/css-tools from 4.0.1 to 4.3.2 in /web-src Bumps [@adobe/css-tools](https://github.com/adobe/css-tools) from 4.0.1 to 4.3.2. - [Changelog](https://github.com/adobe/css-tools/blob/main/History.md) - [Commits](https://github.com/adobe/css-tools/commits) --- updated-dependencies: - dependency-name: "@adobe/css-tools" dependency-type: indirect ... Signed-off-by: dependabot[bot] --- web-src/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/web-src/package-lock.json b/web-src/package-lock.json index e7354e3a..39ca2a70 100644 --- a/web-src/package-lock.json +++ b/web-src/package-lock.json @@ -75,9 +75,9 @@ } }, "node_modules/@adobe/css-tools": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.0.1.tgz", - "integrity": "sha512-+u76oB43nOHrF4DDWRLWDCtci7f3QJoEBigemIdIeTi1ODqjx6Tad9NCVnPRwewWlKkVab5PlK8DCtPTyX7S8g==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.2.tgz", + "integrity": "sha512-DA5a1C0gD/pLOvhv33YMrbf2FK3oUzwNl9oOJqE4XVjuEtt6XIakRcsd7eLiOSPkp1kTRQGICTA8cKra/vFbjw==", "dev": true }, "node_modules/@akryum/winattr": { @@ -25136,9 +25136,9 @@ } }, "@adobe/css-tools": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.0.1.tgz", - "integrity": "sha512-+u76oB43nOHrF4DDWRLWDCtci7f3QJoEBigemIdIeTi1ODqjx6Tad9NCVnPRwewWlKkVab5PlK8DCtPTyX7S8g==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.2.tgz", + "integrity": "sha512-DA5a1C0gD/pLOvhv33YMrbf2FK3oUzwNl9oOJqE4XVjuEtt6XIakRcsd7eLiOSPkp1kTRQGICTA8cKra/vFbjw==", "dev": true }, "@akryum/winattr": { From ccf3dc6c93343537ac9ac43826f0eae49b829075 Mon Sep 17 00:00:00 2001 From: yshepilov Date: Fri, 1 Dec 2023 11:02:01 +0100 Subject: [PATCH 016/148] fixed mjs files compilation --- web-src/vue.config.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web-src/vue.config.js b/web-src/vue.config.js index 71527dd1..9de5836d 100644 --- a/web-src/vue.config.js +++ b/web-src/vue.config.js @@ -59,6 +59,8 @@ module.exports = { const IS_VENDOR = /[\\/]node_modules[\\/]/; + config.resolve.extensions.prepend('.mjs') + // ATTENTION! do not use minSize/maxSize until vue-cli moved to the 4th version of html-webpack-plugin // Otherwise plugin won't be able to find split packages config.optimization From 7ea018023532d42b101fef2ebf7b49ded2ec6b1a Mon Sep 17 00:00:00 2001 From: yshepilov Date: Fri, 1 Dec 2023 11:49:57 +0100 Subject: [PATCH 017/148] fixed mjs files compilation --- web-src/package-lock.json | 12 ++++++------ web-src/vue.config.js | 4 +--- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/web-src/package-lock.json b/web-src/package-lock.json index 39ca2a70..e7354e3a 100644 --- a/web-src/package-lock.json +++ b/web-src/package-lock.json @@ -75,9 +75,9 @@ } }, "node_modules/@adobe/css-tools": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.2.tgz", - "integrity": "sha512-DA5a1C0gD/pLOvhv33YMrbf2FK3oUzwNl9oOJqE4XVjuEtt6XIakRcsd7eLiOSPkp1kTRQGICTA8cKra/vFbjw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.0.1.tgz", + "integrity": "sha512-+u76oB43nOHrF4DDWRLWDCtci7f3QJoEBigemIdIeTi1ODqjx6Tad9NCVnPRwewWlKkVab5PlK8DCtPTyX7S8g==", "dev": true }, "node_modules/@akryum/winattr": { @@ -25136,9 +25136,9 @@ } }, "@adobe/css-tools": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.2.tgz", - "integrity": "sha512-DA5a1C0gD/pLOvhv33YMrbf2FK3oUzwNl9oOJqE4XVjuEtt6XIakRcsd7eLiOSPkp1kTRQGICTA8cKra/vFbjw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.0.1.tgz", + "integrity": "sha512-+u76oB43nOHrF4DDWRLWDCtci7f3QJoEBigemIdIeTi1ODqjx6Tad9NCVnPRwewWlKkVab5PlK8DCtPTyX7S8g==", "dev": true }, "@akryum/winattr": { diff --git a/web-src/vue.config.js b/web-src/vue.config.js index 9de5836d..93b805e1 100644 --- a/web-src/vue.config.js +++ b/web-src/vue.config.js @@ -59,8 +59,6 @@ module.exports = { const IS_VENDOR = /[\\/]node_modules[\\/]/; - config.resolve.extensions.prepend('.mjs') - // ATTENTION! do not use minSize/maxSize until vue-cli moved to the 4th version of html-webpack-plugin // Otherwise plugin won't be able to find split packages config.optimization @@ -109,7 +107,7 @@ module.exports = { 'karma-webpack', 'karma-mocha', 'karma-mocha-reporter', - 'karma-allure-reporter', + 'karma-allure-reporter' ] } } From 9a88f7664e08ce44254016648379dac931b56f07 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jan 2024 06:44:22 +0000 Subject: [PATCH 018/148] Bump follow-redirects from 1.15.2 to 1.15.4 in /web-src Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.2 to 1.15.4. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.2...v1.15.4) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] --- web-src/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/web-src/package-lock.json b/web-src/package-lock.json index e7354e3a..aec692fc 100644 --- a/web-src/package-lock.json +++ b/web-src/package-lock.json @@ -11038,9 +11038,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", + "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", "funding": [ { "type": "individual", @@ -33789,9 +33789,9 @@ } }, "follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", + "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==" }, "for-each": { "version": "0.3.3", From 5a1b1730a88840ce100436679b3b69c0d65dd813 Mon Sep 17 00:00:00 2001 From: Yogendra Singh Date: Fri, 19 Jan 2024 16:05:38 +0530 Subject: [PATCH 019/148] fix: XSS attack via next login parameter. --- web-src/src/login/login.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/web-src/src/login/login.js b/web-src/src/login/login.js index f466ca4f..6ce28f1d 100644 --- a/web-src/src/login/login.js +++ b/web-src/src/login/login.js @@ -34,6 +34,13 @@ function checkRedirectReason() { return redirectReason; } +function validateURL(url) { + if (!url || url.startsWith('http') || url.startsWith('/')) { + return url; + } + return '/'; +} + function onLoad() { axiosInstance.get('auth/config').then(({data: config}) => { const loginContainer = document.getElementById('login-content-container'); @@ -109,7 +116,7 @@ function setupOAuth(loginContainer, authConfig, templateName, buttonId) { 'token': token, 'urlFragment': window.location.hash }; - localState[NEXT_URL_KEY] = getQueryParameter(NEXT_URL_KEY); + localState[NEXT_URL_KEY] = validateURL(getQueryParameter(NEXT_URL_KEY)); saveState(localState); @@ -138,7 +145,7 @@ function processCurrentOauthState() { return; } - var nextUrl = oauthState[NEXT_URL_KEY]; + var nextUrl = validateURL(oauthState[NEXT_URL_KEY]); var urlFragment = oauthState['urlFragment']; var previousLocation = getUnparameterizedUrl(); @@ -177,7 +184,7 @@ function getLoginButton() { function sendLoginRequest(formData) { - var nextUrl = getQueryParameter(NEXT_URL_KEY); + var nextUrl = validateURL(getQueryParameter(NEXT_URL_KEY)); var nextUrlFragment = window.location.hash; if (nextUrl) { From b077c7fbf17557cb13c7a1de755fa5d014f24b08 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Feb 2024 03:47:19 +0000 Subject: [PATCH 020/148] Bump ip from 1.1.8 to 1.1.9 in /web-src Bumps [ip](https://github.com/indutny/node-ip) from 1.1.8 to 1.1.9. - [Commits](https://github.com/indutny/node-ip/compare/v1.1.8...v1.1.9) --- updated-dependencies: - dependency-name: ip dependency-type: indirect ... Signed-off-by: dependabot[bot] --- web-src/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/web-src/package-lock.json b/web-src/package-lock.json index aec692fc..65412c09 100644 --- a/web-src/package-lock.json +++ b/web-src/package-lock.json @@ -12668,9 +12668,9 @@ } }, "node_modules/ip": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", - "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", + "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==", "dev": true }, "node_modules/ip-regex": { @@ -35050,9 +35050,9 @@ } }, "ip": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", - "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", + "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==", "dev": true }, "ip-regex": { From 6ddf2462f94ebd021af3b8cbbdaf7d6f33d393a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 16 Mar 2024 22:49:24 +0000 Subject: [PATCH 021/148] Bump follow-redirects from 1.15.4 to 1.15.6 in /web-src Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.4 to 1.15.6. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.4...v1.15.6) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] --- web-src/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/web-src/package-lock.json b/web-src/package-lock.json index 65412c09..337539f9 100644 --- a/web-src/package-lock.json +++ b/web-src/package-lock.json @@ -11038,9 +11038,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.4", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", - "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", @@ -33789,9 +33789,9 @@ } }, "follow-redirects": { - "version": "1.15.4", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", - "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==" + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==" }, "for-each": { "version": "0.3.3", From 6e8a21105f9d6dbdf83a49290b78b403c28951f3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 Mar 2024 10:58:13 +0000 Subject: [PATCH 022/148] Bump express from 4.18.1 to 4.19.2 in /web-src Bumps [express](https://github.com/expressjs/express) from 4.18.1 to 4.19.2. - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/master/History.md) - [Commits](https://github.com/expressjs/express/compare/4.18.1...4.19.2) --- updated-dependencies: - dependency-name: express dependency-type: indirect ... Signed-off-by: dependabot[bot] --- web-src/package-lock.json | 96 +++++++++++++++++++-------------------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/web-src/package-lock.json b/web-src/package-lock.json index 337539f9..ef55d1e9 100644 --- a/web-src/package-lock.json +++ b/web-src/package-lock.json @@ -6410,21 +6410,21 @@ "dev": true }, "node_modules/body-parser": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", - "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "dev": true, "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.10.3", - "raw-body": "2.5.1", + "qs": "6.11.0", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -8038,9 +8038,9 @@ ] }, "node_modules/content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true, "engines": { "node": ">= 0.6" @@ -8056,9 +8056,9 @@ } }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "dev": true, "engines": { "node": ">= 0.6" @@ -10509,17 +10509,17 @@ } }, "node_modules/express": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", - "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dev": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.0", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -10535,7 +10535,7 @@ "parseurl": "~1.3.3", "path-to-regexp": "0.1.7", "proxy-addr": "~2.0.7", - "qs": "6.10.3", + "qs": "6.11.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.18.0", @@ -18297,9 +18297,9 @@ } }, "node_modules/qs": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", - "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", "dev": true, "dependencies": { "side-channel": "^1.0.4" @@ -18399,9 +18399,9 @@ } }, "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dev": true, "dependencies": { "bytes": "3.1.2", @@ -30056,21 +30056,21 @@ "dev": true }, "body-parser": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", - "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "dev": true, "requires": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.10.3", - "raw-body": "2.5.1", + "qs": "6.11.0", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -31372,9 +31372,9 @@ } }, "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true }, "convert-source-map": { @@ -31387,9 +31387,9 @@ } }, "cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "dev": true }, "cookie-signature": { @@ -33368,17 +33368,17 @@ } }, "express": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", - "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dev": true, "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.0", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -33394,7 +33394,7 @@ "parseurl": "~1.3.3", "path-to-regexp": "0.1.7", "proxy-addr": "~2.0.7", - "qs": "6.10.3", + "qs": "6.11.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.18.0", @@ -39570,9 +39570,9 @@ "dev": true }, "qs": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", - "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", "dev": true, "requires": { "side-channel": "^1.0.4" @@ -39639,9 +39639,9 @@ "dev": true }, "raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dev": true, "requires": { "bytes": "3.1.2", From 1a34ee877fbd9c9abc590b9f73fa5a9af6aec732 Mon Sep 17 00:00:00 2001 From: Lionel Zhang Date: Mon, 1 Apr 2024 14:56:34 -0700 Subject: [PATCH 023/148] feat: add support for azure ad oauth --- src/auth/auth_azure_ad_oauth.py | 34 +++++++++++++++++++++++++++ src/model/server_conf.py | 3 +++ src/tests/server_conf_test.py | 13 ++++++++++ web-src/public/login.html | 8 +++++++ web-src/src/assets/azure-ad-logo.png | Bin 0 -> 1379 bytes web-src/src/assets/css/index.css | 5 ++++ web-src/src/login/login.js | 10 ++++++++ 7 files changed, 73 insertions(+) create mode 100644 src/auth/auth_azure_ad_oauth.py create mode 100644 web-src/src/assets/azure-ad-logo.png diff --git a/src/auth/auth_azure_ad_oauth.py b/src/auth/auth_azure_ad_oauth.py new file mode 100644 index 00000000..5696e634 --- /dev/null +++ b/src/auth/auth_azure_ad_oauth.py @@ -0,0 +1,34 @@ +import logging + +import tornado.auth + +from auth.auth_abstract_oauth import AbstractOauthAuthenticator, _OauthUserInfo +from model import model_helper + +LOGGER = logging.getLogger('script_server.AzureADOauthAuthenticator') + + +class AzureAdOAuthAuthenticator(AbstractOauthAuthenticator): + def __init__(self, params_dict): + params_dict['group_support'] = False + self.auth_url = model_helper.read_obligatory(params_dict, 'auth_url', ' for OAuth') + self.token_url = model_helper.read_obligatory(params_dict, 'token_url', ' for OAuth') + + super().__init__( + self.auth_url, + self.token_url, + 'openid email profile', + params_dict, + ) + + async def fetch_user_info(self, access_token) -> _OauthUserInfo: + headers = {'Authorization': f'Bearer {access_token}'} + user_response = await self.http_client.fetch('https://graph.microsoft.com/v1.0/me', headers=headers) + if not user_response: + return None + + user_data = tornado.escape.json_decode(user_response.body) + return _OauthUserInfo(user_data.get('userPrincipalName'), True, user_data) + + async def fetch_user_groups(self, access_token): + return [] diff --git a/src/model/server_conf.py b/src/model/server_conf.py index b8175486..918a82f1 100644 --- a/src/model/server_conf.py +++ b/src/model/server_conf.py @@ -232,6 +232,9 @@ def create_authenticator(auth_object, temp_folder, process_invoker: ProcessInvok elif auth_type == 'google_oauth': from auth.auth_google_oauth import GoogleOauthAuthenticator authenticator = GoogleOauthAuthenticator(auth_object) + elif auth_type == 'azure_ad_oauth': + from auth.auth_azure_ad_oauth import AzureAdOAuthAuthenticator + authenticator = AzureAdOAuthAuthenticator(auth_object) elif auth_type == 'gitlab': from auth.auth_gitlab import GitlabOAuthAuthenticator authenticator = GitlabOAuthAuthenticator(auth_object) diff --git a/src/tests/server_conf_test.py b/src/tests/server_conf_test.py index 5e63f389..b23f4cb4 100644 --- a/src/tests/server_conf_test.py +++ b/src/tests/server_conf_test.py @@ -6,6 +6,7 @@ from auth.auth_gitlab import GitlabOAuthAuthenticator from auth.auth_google_oauth import GoogleOauthAuthenticator +from auth.auth_azure_ad_oauth import AzureAdOAuthAuthenticator from auth.auth_htpasswd import HtpasswdAuthenticator from auth.auth_ldap import LdapAuthenticator from auth.authorization import ANY_USER @@ -263,6 +264,18 @@ def test_google_oauth_without_allowed_users(self): _from_json({'auth': {'type': 'google_oauth', 'client_id': '1234', 'secret': 'abcd'}}) + + def test_azure_ad_oauth(self): + config = _from_json({'auth': {'type': 'azure_ad_oauth', + 'auth_url': 'https://test.com/authorize', + 'token_url': 'https://test.com/token', + 'client_id': '1234', + 'secret': 'abcd'}}) + self.assertIsInstance(config.authenticator, AzureAdOAuthAuthenticator) + self.assertEquals('https://test.com/authorize', config.authenticator.auth_url) + self.assertEquals('https://test.com/token', config.authenticator.token_url) + self.assertEquals('1234', config.authenticator.client_id) + self.assertEquals('abcd', config.authenticator.secret) def test_gitlab_oauth(self): config = _from_json({ diff --git a/web-src/public/login.html b/web-src/public/login.html index 15c31cdd..9b81a5d3 100644 --- a/web-src/public/login.html +++ b/web-src/public/login.html @@ -45,6 +45,14 @@ + + - \ No newline at end of file + + + + diff --git a/web-src/src/assets/authentik_icon.png b/web-src/src/assets/authentik_icon.png new file mode 100755 index 0000000000000000000000000000000000000000..a6109c915c5c0e7466205dcdbb62efc50b4a4b1a GIT binary patch literal 1686 zcmeAS@N?(olHy`uVBq!ia0vp^AwcZF!3-ofe_Bunq!^2X+?^P2p46!aaySb-B8!2F zuY)k7lg8`{1_mad0G|+7pyXd~-QkV?`za!O@V}=DG8Y&7zZbf)|6Z!N6cZBHA*29X z6nN=@bU{drIQj<_0jt4~MYWC?HP{@0Wb7Yr3@`YgYng3fX9G-6M@oYHf*IHi(%U9Y zX^c~6VN#oU`03~0e?C0e+bzYcviQ~iyUpUNo!?*Qv#$L5w%6pLoZ`l*Y%w>En$G(2 zQgF+Uewkyk#}}=*zvk+rZ|8-U_pE1pUV3FBqZVI#1xISrjOlsK{ER$-3oa@!{@v5@ z;?nk?WsD&^^tuk-6glC}xZ(P~ZGLY8G*5BW*!-%SzVW7q%?s{7^irp{uas>LRE3XXkHhzbtIFAnm)(PU4>`et0&c2s1 zjcE&_6mCYm{?2N&@m}owrv8|QxRDImXvL-!BcpG%^vxrwoZ=bK!mPe0I-+J%EF9xQkIr^HJTB{>?B4-P}K6hVE^hnvVNH@o;US%)sOE0^rheYZ2bY{40o3r=D+F6rty|^;x+`c*Q({9ec z5j)R(`iA#SY^GD^D=xUuByPE=^O1$nt9gHVlviC+pAe97Iw|*uMnJG{kLuQbv#Pzp zg$`dUe=jCD~0LTg4- zGtJ8MzkM5SamI7T)!(n%KlSI`Ukay=-CXx6{7hiU^3oq0Lx0cud24guKj%Me=7r$_ Usy`O&0A_avPgg&ebxsLQ0KQHazW@LL literal 0 HcmV?d00001 diff --git a/web-src/src/assets/css/index.css b/web-src/src/assets/css/index.css index 1aacc54a..d737158a 100644 --- a/web-src/src/assets/css/index.css +++ b/web-src/src/assets/css/index.css @@ -103,7 +103,8 @@ h6.header { #login-panel .login-google_oauth .login-info-label, #login-panel .login-azure_ad_oauth .login-info-label, #login-panel .login-gitlab .login-info-label, -#login-panel .login-keycloak .login-info-label { +#login-panel .login-keycloak .login-info-label, +#login-panel .login-authentik .login-info-label { margin-top: 16px; } @@ -166,3 +167,8 @@ h6.header { padding-left: 42px; background-image: url('../keycloak_icon.png'); } + +#login-authentik-button { + padding-left: 50px; + background-image: url('../authentik_icon.png'); +} diff --git a/web-src/src/login/login.js b/web-src/src/login/login.js index 2215ced3..ab3de274 100644 --- a/web-src/src/login/login.js +++ b/web-src/src/login/login.js @@ -3,7 +3,7 @@ import '@/common/materializecss/imports/cards'; import '@/common/materializecss/imports/input-fields'; import '@/common/style_imports'; import '@/common/style_imports.js'; -import {axiosInstance} from '@/common/utils/axios_utils' +import { axiosInstance } from '@/common/utils/axios_utils' import { addClass, contains, @@ -42,7 +42,7 @@ function validateURL(url) { } function onLoad() { - axiosInstance.get('auth/config').then(({data: config}) => { + axiosInstance.get('auth/config').then(({ data: config }) => { const loginContainer = document.getElementById('login-content-container'); if (config['type'] === 'google_oauth') { @@ -51,6 +51,8 @@ function onLoad() { setupAzureAdOAuth(loginContainer, config); } else if (config['type'] === 'keycloak_openid') { setupKeycloakOpenid(loginContainer, config); + } else if (config['type'] === 'authentik') { + setupAuthentikAuth(loginContainer, config); } else if (config['type'] === 'gitlab') { setupGitlabOAuth(loginContainer, config); } else { @@ -106,6 +108,14 @@ function setupKeycloakOpenid(loginContainer, authConfig) { 'login-keycloak-button') } +function setupAuthentikAuth(loginContainer, authConfig) { + setupOAuth( + loginContainer, + authConfig, + 'login-authentik-template', + 'login-authentik-button') +} + function setupGitlabOAuth(loginContainer, authConfig) { setupOAuth( loginContainer, @@ -160,7 +170,7 @@ function processCurrentOauthState() { var previousLocation = getUnparameterizedUrl(); if (nextUrl) { - previousLocation += '?' + toQueryArgs({'next': nextUrl}); + previousLocation += '?' + toQueryArgs({ 'next': nextUrl }); } if (urlFragment) { previousLocation += urlFragment; @@ -239,7 +249,7 @@ function sendLoginRequest(formData) { const loginButton = getLoginButton(); loginButton.setAttribute('disabled', 'disabled'); - axiosInstance.post(loginUrl, formData, {maxRedirects: 0}) + axiosInstance.post(loginUrl, formData, { maxRedirects: 0 }) .then(onSuccess) .catch(onError) } From 7f3729d0f5dc2771bf5c530575537ecb2b3d86d2 Mon Sep 17 00:00:00 2001 From: knom Date: Mon, 5 May 2025 11:02:23 +0200 Subject: [PATCH 036/148] Changes from the PR review --- src/auth/auth_authentik_openid.py | 2 -- web-src/src/login/login.js | 8 ++++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/auth/auth_authentik_openid.py b/src/auth/auth_authentik_openid.py index 0b9ade21..75256293 100644 --- a/src/auth/auth_authentik_openid.py +++ b/src/auth/auth_authentik_openid.py @@ -19,10 +19,8 @@ def __init__(self, params_dict): authenitk_url = authenitk_url + '/' self._authenitk_url = authenitk_url - # (oauth_authorize_url, oauth_token_url, oauth_scope, params_dict): super().__init__(authenitk_url + 'application/o/authorize/', authenitk_url + 'application/o/token/', - # "openid" scope is needed since version 20: 'email openid profile', params_dict) diff --git a/web-src/src/login/login.js b/web-src/src/login/login.js index ab3de274..207a79a3 100644 --- a/web-src/src/login/login.js +++ b/web-src/src/login/login.js @@ -3,7 +3,7 @@ import '@/common/materializecss/imports/cards'; import '@/common/materializecss/imports/input-fields'; import '@/common/style_imports'; import '@/common/style_imports.js'; -import { axiosInstance } from '@/common/utils/axios_utils' +import {axiosInstance} from '@/common/utils/axios_utils' import { addClass, contains, @@ -42,7 +42,7 @@ function validateURL(url) { } function onLoad() { - axiosInstance.get('auth/config').then(({ data: config }) => { + axiosInstance.get('auth/config').then(({data: config}) => { const loginContainer = document.getElementById('login-content-container'); if (config['type'] === 'google_oauth') { @@ -170,7 +170,7 @@ function processCurrentOauthState() { var previousLocation = getUnparameterizedUrl(); if (nextUrl) { - previousLocation += '?' + toQueryArgs({ 'next': nextUrl }); + previousLocation += '?' + toQueryArgs({'next': nextUrl}); } if (urlFragment) { previousLocation += urlFragment; @@ -249,7 +249,7 @@ function sendLoginRequest(formData) { const loginButton = getLoginButton(); loginButton.setAttribute('disabled', 'disabled'); - axiosInstance.post(loginUrl, formData, { maxRedirects: 0 }) + axiosInstance.post(loginUrl, formData, {maxRedirects: 0}) .then(onSuccess) .catch(onError) } From 36bbf9e68cf6923a7e7b2eea74364ab7de135d54 Mon Sep 17 00:00:00 2001 From: Emmanuel Ferdman Date: Fri, 9 May 2025 16:13:17 -0700 Subject: [PATCH 037/148] migrate to `unittest.assertEqual` Signed-off-by: Emmanuel Ferdman --- src/tests/execution_logging_test.py | 10 +++--- src/tests/model_helper_test.py | 10 +++--- src/tests/server_conf_test.py | 48 ++++++++++++++--------------- src/tests/web/server_test.py | 2 +- 4 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/tests/execution_logging_test.py b/src/tests/execution_logging_test.py index 18be9c1d..79de102a 100644 --- a/src/tests/execution_logging_test.py +++ b/src/tests/execution_logging_test.py @@ -280,7 +280,7 @@ def test_get_history_entries_only_for_current_user(self, user_id): self.simulate_logging(execution_id='id4', user_id='userA') entries = self._get_entries_sorted(user_id) - self.assertEquals(2, len(entries)) + self.assertEqual(2, len(entries)) self.validate_history_entry(entry=entries[0], id='id1', user_id='userA') self.validate_history_entry(entry=entries[1], id='id4', user_id='userA') @@ -292,7 +292,7 @@ def test_get_history_entries_for_power_user(self): self.simulate_logging(execution_id='id4', user_id='userA') entries = self._get_entries_sorted('power_user') - self.assertEquals(4, len(entries)) + self.assertEqual(4, len(entries)) self.validate_history_entry(entry=entries[0], id='id1', user_id='userA') self.validate_history_entry(entry=entries[1], id='id2', user_id='userB') @@ -306,7 +306,7 @@ def test_get_history_entries_for_system_call(self): self.simulate_logging(execution_id='id4', user_id='userA') entries = self._get_entries_sorted('some user', system_call=True) - self.assertEquals(4, len(entries)) + self.assertEqual(4, len(entries)) self.validate_history_entry(entry=entries[0], id='id1', user_id='userA') self.validate_history_entry(entry=entries[1], id='id2', user_id='userB') @@ -357,7 +357,7 @@ def test_entry_with_user_id_name_different(self): entry = self.logging_service.find_history_entry('id1', '192.168.2.12') self.validate_history_entry(entry, id='id1', user_name='userX', user_id='192.168.2.12') - def test_find_entry_when_windows_line_seperator(self): + def test_find_entry_when_windows_line_separator(self): self.simulate_logging(execution_id='id1', user_name='userX', user_id='192.168.2.12') _replace_line_separators(self.get_log_files(), '\n', '\r\n') @@ -381,7 +381,7 @@ def test_find_entry_when_another_user_and_no_entry(self): entry = self.logging_service.find_history_entry('id2', 'userA') self.assertIsNone(entry) - def test_find_log_when_windows_line_seperator(self): + def test_find_log_when_windows_line_separator(self): self.simulate_logging(execution_id='id1', log_lines=['hello', 'wonderful', 'world']) _replace_line_separators(self.get_log_files(), '\n', '\r\n') diff --git a/src/tests/model_helper_test.py b/src/tests/model_helper_test.py index 94a22ce7..9def5ce2 100644 --- a/src/tests/model_helper_test.py +++ b/src/tests/model_helper_test.py @@ -552,7 +552,7 @@ def test_default_value_when_empty_string(self): class TestReadStrFromConfig(unittest.TestCase): def test_normal_text(self): value = read_str_from_config({'key1': 'xyz'}, 'key1') - self.assertEquals('xyz', value) + self.assertEqual('xyz', value) def test_none_value_no_default(self): value = read_str_from_config({'key1': None}, 'key1') @@ -560,7 +560,7 @@ def test_none_value_no_default(self): def test_none_value_with_default(self): value = read_str_from_config({'key1': None}, 'key1', default='abc') - self.assertEquals('abc', value) + self.assertEqual('abc', value) def test_no_key_no_default(self): value = read_str_from_config({'key1': 'xyz'}, 'key2') @@ -568,11 +568,11 @@ def test_no_key_no_default(self): def test_no_key_with_default(self): value = read_str_from_config({'key1': 'xyz'}, 'key2', default='abc') - self.assertEquals('abc', value) + self.assertEqual('abc', value) def test_text_with_whitespaces(self): value = read_str_from_config({'key1': ' xyz \n'}, 'key1') - self.assertEquals(' xyz \n', value) + self.assertEqual(' xyz \n', value) def test_text_when_blank_to_none_and_none(self): value = read_str_from_config({'key1': None}, 'key1', blank_to_none=True) @@ -588,7 +588,7 @@ def test_text_when_blank_to_none_and_blank(self): def test_text_when_blank_to_none_and_blank_and_default(self): value = read_str_from_config({'key1': ' \t \n'}, 'key1', blank_to_none=True, default='abc') - self.assertEquals('abc', value) + self.assertEqual('abc', value) def test_text_when_int(self): self.assertRaisesRegex(InvalidValueTypeException, 'Invalid key1 value: string expected, but was: 5', diff --git a/src/tests/server_conf_test.py b/src/tests/server_conf_test.py index b23f4cb4..467fb583 100644 --- a/src/tests/server_conf_test.py +++ b/src/tests/server_conf_test.py @@ -256,8 +256,8 @@ def test_google_oauth(self): 'allowed_users': [] }}) self.assertIsInstance(config.authenticator, GoogleOauthAuthenticator) - self.assertEquals('1234', config.authenticator.client_id) - self.assertEquals('abcd', config.authenticator.secret) + self.assertEqual('1234', config.authenticator.client_id) + self.assertEqual('abcd', config.authenticator.secret) def test_google_oauth_without_allowed_users(self): with self.assertRaisesRegex(Exception, 'access.allowed_users field is mandatory for google_oauth'): @@ -272,10 +272,10 @@ def test_azure_ad_oauth(self): 'client_id': '1234', 'secret': 'abcd'}}) self.assertIsInstance(config.authenticator, AzureAdOAuthAuthenticator) - self.assertEquals('https://test.com/authorize', config.authenticator.auth_url) - self.assertEquals('https://test.com/token', config.authenticator.token_url) - self.assertEquals('1234', config.authenticator.client_id) - self.assertEquals('abcd', config.authenticator.secret) + self.assertEqual('https://test.com/authorize', config.authenticator.auth_url) + self.assertEqual('https://test.com/token', config.authenticator.token_url) + self.assertEqual('1234', config.authenticator.client_id) + self.assertEqual('abcd', config.authenticator.secret) def test_gitlab_oauth(self): config = _from_json({ @@ -297,18 +297,18 @@ def test_ldap(self): 'base_dn': 'dc=test', 'version': 3}}) self.assertIsInstance(config.authenticator, LdapAuthenticator) - self.assertEquals('http://test-ldap.net', config.authenticator.url) - self.assertEquals('|xyz|', config.authenticator.username_template.substitute(username='xyz')) - self.assertEquals('dc=test', config.authenticator._base_dn) - self.assertEquals(3, config.authenticator.version) + self.assertEqual('http://test-ldap.net', config.authenticator.url) + self.assertEqual('|xyz|', config.authenticator.username_template.substitute(username='xyz')) + self.assertEqual('dc=test', config.authenticator._base_dn) + self.assertEqual(3, config.authenticator.version) def test_ldap_multiple_urls(self): config = _from_json({'auth': {'type': 'ldap', 'url': ['http://test-ldap-1.net', 'http://test-ldap-2.net'], 'username_pattern': '|$username|'}}) self.assertIsInstance(config.authenticator, LdapAuthenticator) - self.assertEquals(['http://test-ldap-1.net', 'http://test-ldap-2.net'], config.authenticator.url) - self.assertEquals('|xyz|', config.authenticator.username_template.substitute(username='xyz')) + self.assertEqual(['http://test-ldap-1.net', 'http://test-ldap-2.net'], config.authenticator.url) + self.assertEqual('|xyz|', config.authenticator.username_template.substitute(username='xyz')) def test_htpasswd_auth(self): file = test_utils.create_file('some-path', text='user1:1yL79Q78yczsM') @@ -332,7 +332,7 @@ class TestSecurityConfig(unittest.TestCase): def test_default_config(self): config = _from_json({}) - self.assertEquals('token', config.xsrf_protection) + self.assertEqual('token', config.xsrf_protection) @parameterized.expand([ ('token',), @@ -344,7 +344,7 @@ def test_xsrf_protection(self, xsrf_protection): 'xsrf_protection': xsrf_protection }}) - self.assertEquals(xsrf_protection, config.xsrf_protection) + self.assertEqual(xsrf_protection, config.xsrf_protection) def test_xsrf_protection_when_unsupported(self): self.assertRaises(InvalidValueException, _from_json, {'security': { @@ -375,18 +375,18 @@ def tearDown(self): def test_default_config(self): config = _from_json({}) env_vars = config.env_vars.build_env_vars() - self.assertEquals(env_vars, os.environ) + self.assertEqual(env_vars, os.environ) def test_config_when_safe_env_variables_used(self): config = _from_json({'title': '$$VAR1', 'auth': {'type': 'ldap', 'url': '$$MY_SECRET'}}) env_vars = config.env_vars.build_env_vars() - self.assertEquals(env_vars, os.environ) + self.assertEqual(env_vars, os.environ) self.assertEqual('abcd', env_vars['VAR1']) self.assertEqual('qwerty', env_vars['MY_SECRET']) - self.assertEquals(config.title, '$$VAR1') - self.assertEquals(config.authenticator.url, '$$MY_SECRET') + self.assertEqual(config.title, '$$VAR1') + self.assertEqual(config.authenticator.url, '$$MY_SECRET') def test_config_when_unsafe_env_variables_used(self): config = _from_json({ @@ -410,18 +410,18 @@ def test_config_when_unsafe_env_variables_used(self): self.assertNotIn('EMAIL_PWD', env_vars) self.assertNotIn('EMAIL_PWD_2', env_vars) - self.assertEquals(config.title, '$$VAR1') - self.assertEquals(config.authenticator.secret, 'qwerty') + self.assertEqual(config.title, '$$VAR1') + self.assertEqual(config.authenticator.secret, 'qwerty') alert_destinations = AlertsService(config.alerts_config)._communication_service._destinations - self.assertEquals(alert_destinations[0]._communicator.password, '1234509') - self.assertEquals(alert_destinations[1]._communicator.password, '$VAR2') + self.assertEqual(alert_destinations[0]._communicator.password, '1234509') + self.assertEqual(alert_destinations[1]._communicator.password, '$VAR2') # noinspection PyTypeChecker callback_feature = ExecutionsCallbackFeature(None, config.callbacks_config, None) callback_destinations = callback_feature._communication_service._destinations - self.assertEquals(callback_destinations[0]._communicator.password, '007') - self.assertEquals(callback_destinations[1]._communicator.password, 'VAR1') + self.assertEqual(callback_destinations[0]._communicator.password, '007') + self.assertEqual(callback_destinations[1]._communicator.password, 'VAR1') def create_email_destination(self, password): return {'type': 'email', diff --git a/src/tests/web/server_test.py b/src/tests/web/server_test.py index 7a4abbbe..59fdceb6 100644 --- a/src/tests/web/server_test.py +++ b/src/tests/web/server_test.py @@ -253,7 +253,7 @@ def test_get_scripts_when_basic_auth_failure(self): test_utils.write_script_config({'name': 's1'}, 's1', self.runners_folder) response = requests.get('http://127.0.0.1:12345/scripts', auth=HTTPBasicAuth('normal_user', 'wrong_pass')) - self.assertEquals(401, response.status_code) + self.assertEqual(401, response.status_code) @staticmethod def get_xsrf_token(session): From 63b88ea46555895ced42a888e71bf75e05df2dba Mon Sep 17 00:00:00 2001 From: "daniel.engelmann" Date: Fri, 11 Jul 2025 08:24:14 +0200 Subject: [PATCH 038/148] Implemented History --- web-src/src/common/components/log_panel.vue | 26 +++ web-src/src/common/utils/parameterHistory.js | 108 +++++++++++ .../scripts/ParameterHistoryModal.vue | 170 ++++++++++++++++++ .../components/scripts/script-view.vue | 36 +++- .../main-app/store/scriptExecutionManager.js | 6 +- web-src/src/main-app/store/scriptSetup.js | 34 +++- 6 files changed, 372 insertions(+), 8 deletions(-) create mode 100644 web-src/src/common/utils/parameterHistory.js create mode 100644 web-src/src/main-app/components/scripts/ParameterHistoryModal.vue diff --git a/web-src/src/common/components/log_panel.vue b/web-src/src/common/components/log_panel.vue index ba24165f..6bd713b4 100644 --- a/web-src/src/common/components/log_panel.vue +++ b/web-src/src/common/components/log_panel.vue @@ -9,6 +9,9 @@ content_copy + + arrow_downward + @@ -124,6 +127,19 @@ export default { copyToClipboard(this.output.element); }, + downloadLog: function () { + const content = this.output.element.innerText || this.output.element.textContent || ''; + const blob = new Blob([content], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'log-output.txt'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }, + renderOutputElement: function () { if (!this.output || !this.$el) { return @@ -233,6 +249,16 @@ export default { color: var(--font-color-disabled); } +.log-panel .download-text-button { + position: absolute; + right: 50px; + bottom: 4px; +} + +.log-panel .download-text-button i { + color: var(--font-color-disabled); +} + /*noinspection CssInvalidPropertyValue,CssOverwrittenProperties*/ .log-panel >>> .log-content { display: block; diff --git a/web-src/src/common/utils/parameterHistory.js b/web-src/src/common/utils/parameterHistory.js new file mode 100644 index 00000000..f28373e5 --- /dev/null +++ b/web-src/src/common/utils/parameterHistory.js @@ -0,0 +1,108 @@ +/** + * Parameter History Utility + * Manages saving and loading of parameter values to/from localStorage + */ + +const PARAMETER_HISTORY_PREFIX = 'script_server_param_history_'; +const MAX_HISTORY_ENTRIES = 10; + +/** + * Get the storage key for a specific script's parameter history + * @param {string} scriptName - The name of the script + * @returns {string} The storage key + */ +function getStorageKey(scriptName) { + return PARAMETER_HISTORY_PREFIX + scriptName; +} + +/** + * Save parameter values to localStorage for a specific script + * @param {string} scriptName - The name of the script + * @param {Object} parameterValues - The parameter values to save + */ +export function saveParameterHistory(scriptName, parameterValues) { + try { + const key = getStorageKey(scriptName); + const history = loadParameterHistory(scriptName); + + // check if parameterValues is empty + if (Object.keys(parameterValues).length === 0) { + return; + } + + // Add current values to history (avoid duplicates) + const newEntry = { + timestamp: Date.now(), + values: { ...parameterValues } + }; + + // Remove any existing entry with the same values + const filteredHistory = history.filter(entry => + JSON.stringify(entry.values) !== JSON.stringify(newEntry.values) + ); + + // Add new entry at the beginning + filteredHistory.unshift(newEntry); + + // Keep only the most recent entries + if (filteredHistory.length > MAX_HISTORY_ENTRIES) { + filteredHistory.splice(MAX_HISTORY_ENTRIES); + } + + localStorage.setItem(key, JSON.stringify(filteredHistory)); + } catch (error) { + console.warn('Failed to save parameter history:', error); + } +} + +/** + * Load parameter history from localStorage for a specific script + * @param {string} scriptName - The name of the script + * @returns {Array} Array of historical parameter entries + */ +export function loadParameterHistory(scriptName) { + try { + const key = getStorageKey(scriptName); + const stored = localStorage.getItem(key); + return stored ? JSON.parse(stored) : []; + } catch (error) { + console.warn('Failed to load parameter history:', error); + return []; + } +} + +/** + * Get the most recent parameter values for a script + * @param {string} scriptName - The name of the script + * @returns {Object|null} The most recent parameter values or null if no history + */ +export function getMostRecentValues(scriptName) { + const history = loadParameterHistory(scriptName); + return history.length > 0 ? history[0].values : null; +} + +/** + * Remove a specific parameter history entry for a script + * @param {string} scriptName - The name of the script + * @param {number} index - The index of the entry to remove (0-based) + */ +export function removeParameterHistoryEntry(scriptName, index) { + try { + const key = getStorageKey(scriptName); + const history = loadParameterHistory(scriptName); + + // Check if the index is valid + if (index < 0 || index >= history.length) { + console.warn(`Invalid index: ${index} for script: ${scriptName}`); + return; + } + + // Remove the entry at the specified index + history.splice(index, 1); + + // Save the updated history back to localStorage + localStorage.setItem(key, JSON.stringify(history)); + } catch (error) { + console.warn('Failed to remove parameter history entry:', error); + } +} \ No newline at end of file diff --git a/web-src/src/main-app/components/scripts/ParameterHistoryModal.vue b/web-src/src/main-app/components/scripts/ParameterHistoryModal.vue new file mode 100644 index 00000000..c4a8680d --- /dev/null +++ b/web-src/src/main-app/components/scripts/ParameterHistoryModal.vue @@ -0,0 +1,170 @@ + + + + + \ No newline at end of file diff --git a/web-src/src/main-app/components/scripts/script-view.vue b/web-src/src/main-app/components/scripts/script-view.vue index 6288c991..fa662e92 100644 --- a/web-src/src/main-app/components/scripts/script-view.vue +++ b/web-src/src/main-app/components/scripts/script-view.vue @@ -19,6 +19,12 @@ @click="stopScript"> {{ stopButtonLabel }} +
@@ -54,6 +60,7 @@ ref="scheduleHolder" :scriptConfigComponentsHeight="scriptConfigComponentsHeight" @close="scheduleMode = false"/> + @@ -64,6 +71,7 @@ import {deepCloneObject, forEachKeyValue, isEmptyObject, isEmptyString, isNull} import ScheduleButton from '@/main-app/components/scripts/ScheduleButton'; import ScriptLoadingText from '@/main-app/components/scripts/ScriptLoadingText'; import ScriptViewScheduleHolder from '@/main-app/components/scripts/ScriptViewScheduleHolder'; +import ParameterHistoryModal from '@/main-app/components/scripts/ParameterHistoryModal'; import DOMPurify from 'dompurify'; import {marked} from 'marked'; import {mapActions, mapState} from 'vuex' @@ -96,7 +104,8 @@ export default { LogPanel, ScriptParametersView, ScheduleButton, - ScriptViewScheduleHolder + ScriptViewScheduleHolder, + ParameterHistoryModal }, computed: { @@ -328,6 +337,17 @@ export default { appendLog: function (text) { this.$refs.logPanel.appendLog(text); + }, + + openParameterHistory() { + this.$refs.parameterHistoryModal.open(); + }, + + handleUseParameters(values) { + // Set all parameter values using the scriptSetup store + for (const [parameterName, value] of Object.entries(values)) { + this.$store.dispatch('scriptSetup/setParameterValue', { parameterName, value }); + } } }, @@ -492,6 +512,20 @@ export default { color: var(--font-on-primary-color-main) } +.button-history { + margin-left: 16px; + flex: 1 1 auto; + color: var(--primary-color); + border: 1px solid var(--outline-color); + display: flex; + align-items: center; + gap: 8px; +} + +.button-history i { + font-size: 18px; +} + .schedule-button { margin-left: 32px; flex: 1 0 auto; diff --git a/web-src/src/main-app/store/scriptExecutionManager.js b/web-src/src/main-app/store/scriptExecutionManager.js index 2d0a89a9..839b3983 100644 --- a/web-src/src/main-app/store/scriptExecutionManager.js +++ b/web-src/src/main-app/store/scriptExecutionManager.js @@ -10,7 +10,8 @@ import scriptExecutor, { STATUS_INITIALIZING } from './scriptExecutor'; import {parametersToFormData} from '@/main-app/store/mainStoreHelper'; -import axios from 'axios' +import axios from 'axios'; +import { saveParameterHistory } from '@/common/utils/parameterHistory'; export default { namespaced: true, @@ -122,6 +123,9 @@ export default { const parameterValues = clone(rootState.scriptSetup.parameterValues); const scriptName = rootState.scriptConfig.scriptConfig.name; + // Save parameter history when script is executed + saveParameterHistory(scriptName, parameterValues); + const formData = parametersToFormData(parameterValues); formData.append('__script_name', scriptName); diff --git a/web-src/src/main-app/store/scriptSetup.js b/web-src/src/main-app/store/scriptSetup.js index 2a10fd46..eae68dc2 100644 --- a/web-src/src/main-app/store/scriptSetup.js +++ b/web-src/src/main-app/store/scriptSetup.js @@ -10,6 +10,7 @@ import { import clone from 'lodash/clone'; import isEqual from 'lodash/isEqual'; import Vue from 'vue'; +import { getMostRecentValues } from '@/common/utils/parameterHistory'; export default { namespaced: true, @@ -73,13 +74,34 @@ export default { return; } - const values = {}; - for (const parameter of parameters) { - const defaultValue = !isNull(parameter.default) ? parameter.default : null; - values[parameter.name] = defaultValue; + // Try to load historical values first + const historicalValues = scriptConfig ? getMostRecentValues(scriptConfig.name) : null; + let values = {}; - if (!isNull(values[parameter.name])) { - commit('MEMORIZE_DEFAULT_VALUE', {parameterName: parameter.name, defaultValue}); + if (historicalValues) { + // Only use historical values for parameters that exist in current config + for (const parameter of parameters) { + const parameterName = parameter.name; + if (historicalValues.hasOwnProperty(parameterName)) { + values[parameterName] = historicalValues[parameterName]; + } else { + const defaultValue = !isNull(parameter.default) ? parameter.default : null; + values[parameterName] = defaultValue; + } + + if (!isNull(values[parameterName])) { + commit('MEMORIZE_DEFAULT_VALUE', {parameterName: parameter.name, defaultValue: values[parameterName]}); + } + } + } else { + // No historical values, use defaults + for (const parameter of parameters) { + const defaultValue = !isNull(parameter.default) ? parameter.default : null; + values[parameter.name] = defaultValue; + + if (!isNull(values[parameter.name])) { + commit('MEMORIZE_DEFAULT_VALUE', {parameterName: parameter.name, defaultValue}); + } } } From 5f949b7c4505b4d59429c03f79d5287aed49a7af Mon Sep 17 00:00:00 2001 From: "daniel.engelmann" Date: Fri, 11 Jul 2025 09:53:57 +0200 Subject: [PATCH 039/148] added favorite button --- web-src/src/common/utils/parameterHistory.js | 90 ++++++++++++++++--- .../scripts/ParameterHistoryModal.vue | 50 ++++++++++- 2 files changed, 126 insertions(+), 14 deletions(-) diff --git a/web-src/src/common/utils/parameterHistory.js b/web-src/src/common/utils/parameterHistory.js index f28373e5..1b7a5801 100644 --- a/web-src/src/common/utils/parameterHistory.js +++ b/web-src/src/common/utils/parameterHistory.js @@ -33,23 +33,41 @@ export function saveParameterHistory(scriptName, parameterValues) { // Add current values to history (avoid duplicates) const newEntry = { timestamp: Date.now(), - values: { ...parameterValues } + values: { ...parameterValues }, + favorite: false }; - // Remove any existing entry with the same values - const filteredHistory = history.filter(entry => - JSON.stringify(entry.values) !== JSON.stringify(newEntry.values) + // Check if an entry with the same values already exists + const existingEntryIndex = history.findIndex(entry => + JSON.stringify(entry.values) === JSON.stringify(newEntry.values) ); - // Add new entry at the beginning - filteredHistory.unshift(newEntry); + let filteredHistory; + if (existingEntryIndex !== -1) { + // If entry exists, preserve its favorite status and update timestamp + filteredHistory = [...history]; + filteredHistory[existingEntryIndex] = { + ...filteredHistory[existingEntryIndex], + timestamp: Date.now() + }; + } else { + // If no duplicate exists, add new entry at the beginning + filteredHistory = [newEntry, ...history]; + } + + // Keep only the most recent entries (excluding favorites) + const nonFavoriteEntries = filteredHistory.filter(entry => !entry.favorite); + const favoriteEntries = filteredHistory.filter(entry => entry.favorite); - // Keep only the most recent entries - if (filteredHistory.length > MAX_HISTORY_ENTRIES) { - filteredHistory.splice(MAX_HISTORY_ENTRIES); + // Limit non-favorite entries + if (nonFavoriteEntries.length > MAX_HISTORY_ENTRIES) { + nonFavoriteEntries.splice(MAX_HISTORY_ENTRIES); } - localStorage.setItem(key, JSON.stringify(filteredHistory)); + // Combine favorites first, then non-favorites + const finalHistory = [...favoriteEntries, ...nonFavoriteEntries]; + + localStorage.setItem(key, JSON.stringify(finalHistory)); } catch (error) { console.warn('Failed to save parameter history:', error); } @@ -64,7 +82,13 @@ export function loadParameterHistory(scriptName) { try { const key = getStorageKey(scriptName); const stored = localStorage.getItem(key); - return stored ? JSON.parse(stored) : []; + const history = stored ? JSON.parse(stored) : []; + + // Ensure all entries have the favorite property (for backward compatibility) + return history.map(entry => ({ + ...entry, + favorite: entry.favorite || false + })); } catch (error) { console.warn('Failed to load parameter history:', error); return []; @@ -97,6 +121,12 @@ export function removeParameterHistoryEntry(scriptName, index) { return; } + // Don't allow removal of favorite entries + if (history[index].favorite) { + console.warn('Cannot remove favorite entry'); + return; + } + // Remove the entry at the specified index history.splice(index, 1); @@ -105,4 +135,42 @@ export function removeParameterHistoryEntry(scriptName, index) { } catch (error) { console.warn('Failed to remove parameter history entry:', error); } +} + +/** + * Toggle favorite status of a parameter history entry + * @param {string} scriptName - The name of the script + * @param {number} index - The index of the entry to toggle (0-based) + */ +export function toggleFavoriteEntry(scriptName, index) { + try { + const key = getStorageKey(scriptName); + const history = loadParameterHistory(scriptName); + + // Check if the index is valid + if (index < 0 || index >= history.length) { + console.warn(`Invalid index: ${index} for script: ${scriptName}`); + return; + } + + // Toggle favorite status + history[index].favorite = !history[index].favorite; + + // Reorder entries: favorites first, then non-favorites + const favoriteEntries = history.filter(entry => entry.favorite); + const nonFavoriteEntries = history.filter(entry => !entry.favorite); + + // Limit non-favorite entries + if (nonFavoriteEntries.length > MAX_HISTORY_ENTRIES) { + nonFavoriteEntries.splice(MAX_HISTORY_ENTRIES); + } + + // Combine favorites first, then non-favorites + const finalHistory = [...favoriteEntries, ...nonFavoriteEntries]; + + // Save the updated history back to localStorage + localStorage.setItem(key, JSON.stringify(finalHistory)); + } catch (error) { + console.warn('Failed to toggle favorite entry:', error); + } } \ No newline at end of file diff --git a/web-src/src/main-app/components/scripts/ParameterHistoryModal.vue b/web-src/src/main-app/components/scripts/ParameterHistoryModal.vue index c4a8680d..f3462036 100644 --- a/web-src/src/main-app/components/scripts/ParameterHistoryModal.vue +++ b/web-src/src/main-app/components/scripts/ParameterHistoryModal.vue @@ -7,10 +7,16 @@
-
+
{{ formatTimestamp(entry.timestamp) }}
+
@@ -37,7 +44,7 @@ \ No newline at end of file diff --git a/web-src/src/main-app/components/scripts/script-view.vue b/web-src/src/main-app/components/scripts/script-view.vue index fa662e92..85757937 100644 --- a/web-src/src/main-app/components/scripts/script-view.vue +++ b/web-src/src/main-app/components/scripts/script-view.vue @@ -19,12 +19,6 @@ @click="stopScript"> {{ stopButtonLabel }} -
@@ -60,7 +54,6 @@ ref="scheduleHolder" :scriptConfigComponentsHeight="scriptConfigComponentsHeight" @close="scheduleMode = false"/> -
@@ -71,7 +64,6 @@ import {deepCloneObject, forEachKeyValue, isEmptyObject, isEmptyString, isNull} import ScheduleButton from '@/main-app/components/scripts/ScheduleButton'; import ScriptLoadingText from '@/main-app/components/scripts/ScriptLoadingText'; import ScriptViewScheduleHolder from '@/main-app/components/scripts/ScriptViewScheduleHolder'; -import ParameterHistoryModal from '@/main-app/components/scripts/ParameterHistoryModal'; import DOMPurify from 'dompurify'; import {marked} from 'marked'; import {mapActions, mapState} from 'vuex' @@ -104,8 +96,7 @@ export default { LogPanel, ScriptParametersView, ScheduleButton, - ScriptViewScheduleHolder, - ParameterHistoryModal + ScriptViewScheduleHolder }, computed: { @@ -339,16 +330,7 @@ export default { this.$refs.logPanel.appendLog(text); }, - openParameterHistory() { - this.$refs.parameterHistoryModal.open(); - }, - handleUseParameters(values) { - // Set all parameter values using the scriptSetup store - for (const [parameterName, value] of Object.entries(values)) { - this.$store.dispatch('scriptSetup/setParameterValue', { parameterName, value }); - } - } }, watch: { @@ -512,20 +494,6 @@ export default { color: var(--font-on-primary-color-main) } -.button-history { - margin-left: 16px; - flex: 1 1 auto; - color: var(--primary-color); - border: 1px solid var(--outline-color); - display: flex; - align-items: center; - gap: 8px; -} - -.button-history i { - font-size: 18px; -} - .schedule-button { margin-left: 32px; flex: 1 0 auto; From 6a42a79955ec30b2d3d5f7204e6987304fa4036c Mon Sep 17 00:00:00 2001 From: "daniel.engelmann" Date: Fri, 1 Aug 2025 16:36:53 +0200 Subject: [PATCH 042/148] removed boarder and background of parameter history button --- web-src/src/main-app/components/scripts/ScriptHeader.vue | 2 -- 1 file changed, 2 deletions(-) diff --git a/web-src/src/main-app/components/scripts/ScriptHeader.vue b/web-src/src/main-app/components/scripts/ScriptHeader.vue index e4d62bfe..a79b8359 100644 --- a/web-src/src/main-app/components/scripts/ScriptHeader.vue +++ b/web-src/src/main-app/components/scripts/ScriptHeader.vue @@ -70,8 +70,6 @@ export default { .button-history { margin-right: 16px; flex: 0 0 auto; - color: var(--primary-color); - border: 1px solid var(--outline-color); display: flex; align-items: center; justify-content: center; From 6710ad006aefbf628ead6b6513aedaa4fa0e9d09 Mon Sep 17 00:00:00 2001 From: "daniel.engelmann" Date: Mon, 4 Aug 2025 10:09:35 +0200 Subject: [PATCH 043/148] styling and added toggle for preloading of history parameters --- web-src/src/common/components/log_panel.vue | 2 +- web-src/src/common/utils/parameterHistory.js | 14 ++++++++ .../scripts/ParameterHistoryModal.vue | 32 ++++++++++++++++--- .../components/scripts/ScriptHeader.vue | 8 ++--- web-src/src/main-app/store/scriptSetup.js | 4 +-- 5 files changed, 48 insertions(+), 12 deletions(-) diff --git a/web-src/src/common/components/log_panel.vue b/web-src/src/common/components/log_panel.vue index 6bd713b4..401b07b1 100644 --- a/web-src/src/common/components/log_panel.vue +++ b/web-src/src/common/components/log_panel.vue @@ -10,7 +10,7 @@ content_copy - arrow_downward + file_download
diff --git a/web-src/src/common/utils/parameterHistory.js b/web-src/src/common/utils/parameterHistory.js index 1b7a5801..4a4b8f9b 100644 --- a/web-src/src/common/utils/parameterHistory.js +++ b/web-src/src/common/utils/parameterHistory.js @@ -173,4 +173,18 @@ export function toggleFavoriteEntry(scriptName, index) { } catch (error) { console.warn('Failed to toggle favorite entry:', error); } +} + +/** + * Check if historical values should be used for a script + * @param {string} scriptName - The name of the script + * @returns {boolean} True if historical values should be used, false otherwise + */ +export function shouldUseHistoricalValues(scriptName) { + try { + return localStorage.getItem(`useHistoricalValues_${scriptName}`) === 'true'; + } catch (error) { + console.warn('Failed to check historical values toggle:', error); + return false; + } } \ No newline at end of file diff --git a/web-src/src/main-app/components/scripts/ParameterHistoryModal.vue b/web-src/src/main-app/components/scripts/ParameterHistoryModal.vue index f3462036..38ac2501 100644 --- a/web-src/src/main-app/components/scripts/ParameterHistoryModal.vue +++ b/web-src/src/main-app/components/scripts/ParameterHistoryModal.vue @@ -2,6 +2,17 @@ +
+ +
{ @@ -256,7 +262,9 @@ export default { uiWidthWeight: null, uiSeparatorType: null, uiSeparatorTitle: null, + dateFormat: null, nameField, + dateFormatField, paramField: Object.assign({}, paramField), passAsField, stdinExpectedTextField, @@ -318,6 +326,7 @@ export default { this.stdinExpectedText = get(config, 'stdin_expected_text') this.passAs = get(config, 'pass_as', 'argument + env_variable') this.valuesUiMapping = get(config, 'values_ui_mapping', {}) + this.dateFormat = get(config, 'date_format', '') this.uiWidthWeight = config['ui']?.['width_weight'] this.uiSeparatorType = config['ui']?.['separator_before']?.['type'] diff --git a/web-src/src/admin/components/scripts-config/parameter-fields.js b/web-src/src/admin/components/scripts-config/parameter-fields.js index 5e2e8a4b..d5a8edca 100644 --- a/web-src/src/admin/components/scripts-config/parameter-fields.js +++ b/web-src/src/admin/components/scripts-config/parameter-fields.js @@ -40,6 +40,7 @@ export const typeField = { values: [ 'text', 'int', + 'date', 'list', 'multiselect', 'editable_list', @@ -51,6 +52,11 @@ export const typeField = { 'ip6'] }; +export const dateFormatField = { + name: 'Date format', + description: 'Python strftime format for the date passed to the script (e.g. %Y-%m-%d, %d/%m/%Y, %Y%m%d). Default: %Y-%m-%d' +}; + export const noValueField = { name: 'Without value', withoutValue: true, diff --git a/web-src/src/common/components/inputs/DateField.vue b/web-src/src/common/components/inputs/DateField.vue new file mode 100644 index 00000000..edfbe154 --- /dev/null +++ b/web-src/src/common/components/inputs/DateField.vue @@ -0,0 +1,89 @@ + + + + + diff --git a/web-src/src/main-app/components/scripts/script-parameters-view.vue b/web-src/src/main-app/components/scripts/script-parameters-view.vue index 1287ceb6..b7c95aaa 100644 --- a/web-src/src/main-app/components/scripts/script-parameters-view.vue +++ b/web-src/src/main-app/components/scripts/script-parameters-view.vue @@ -24,6 +24,7 @@ + + diff --git a/web-src/src/main-app/components/scripts/script-parameters-view.vue b/web-src/src/main-app/components/scripts/script-parameters-view.vue index b7c95aaa..a8783f60 100644 --- a/web-src/src/main-app/components/scripts/script-parameters-view.vue +++ b/web-src/src/main-app/components/scripts/script-parameters-view.vue @@ -25,6 +25,7 @@ import Checkbox from '@/common/components/checkbox' import Combobox from '@/common/components/combobox' import DateField from '@/common/components/inputs/DateField' +import TimeField from '@/common/components/inputs/TimeField' import FileUpload from '@/common/components/file_upload' import ServerFileField from '@/common/components/server_file_field' import TextArea from '@/common/components/TextArea' @@ -82,6 +83,8 @@ export default { return FileUpload; } else if (parameter.type === 'date') { return DateField; + } else if (parameter.type === 'time') { + return TimeField; } else if (parameter.type === 'multiline_text') { return TextArea; } else { From d2b081d3c2fd962d2dd323c7f3778d98e0b9f017 Mon Sep 17 00:00:00 2001 From: Thomas Kpenou Date: Thu, 28 May 2026 06:38:08 -0400 Subject: [PATCH 072/148] test: scale Keycloak token TTLs to fix timing flake on CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The background userinfo call (which updates groups after a token refresh) was racing against a 0.1 s access-token TTL. On a loaded Python 3.10 CI runner the coroutine sometimes started after the fresh token had already expired, producing a 401 and leaving groups empty. Fix: raise access_expiration_duration 0.1 → 0.5 s and refresh_expiration_duration 0.6 → 2.0 s so issued tokens outlive any realistic event-loop scheduling delay. The hardcoded sleep in test_success_validate_after_refresh is changed from the magic number 0.5 s to `access_expiration_duration + 0.2` so it stays valid as the constant changes. All other tests already reference the module-level constants directly. Co-Authored-By: Claude Sonnet 4.6 --- src/tests/auth/test_auth_keycloak_openid.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tests/auth/test_auth_keycloak_openid.py b/src/tests/auth/test_auth_keycloak_openid.py index 765df83b..23cd43cb 100644 --- a/src/tests/auth/test_auth_keycloak_openid.py +++ b/src/tests/auth/test_auth_keycloak_openid.py @@ -16,8 +16,8 @@ REALM_URL = 'http://my-keycloak.net/realms/master' -access_expiration_duration = 0.1 -refresh_expiration_duration = 0.6 +access_expiration_duration = 0.5 +refresh_expiration_duration = 2.0 class OauthServerMock: @@ -187,7 +187,7 @@ async def test_success_validate_after_refresh(self): self.oauth_server.set_groups('bugy', ['g3']) - await gen.sleep(0.4 + 0.1) + await gen.sleep(access_expiration_duration + 0.2) valid_1 = await self.authenticator.validate_user(username, mock_request_handler(previous_request=request_1)) self.assertTrue(valid_1) From 88a7c3ec65539573881733853162ac6631b81b13 Mon Sep 17 00:00:00 2001 From: Thomas Kpenou Date: Thu, 28 May 2026 06:58:58 -0400 Subject: [PATCH 073/148] test: fix Keycloak auth timing to prevent flaky CI failures Introduce auth_info_ttl=1.0s constant so the test_read_tokens_from_request sleep (0.6s) stays below the TTL threshold and test_success_validate_after_refresh sleep (1.5s) stays above it, giving sufficient margin on both sides for CI jitter. Co-Authored-By: Claude Sonnet 4.6 --- src/tests/auth/test_auth_keycloak_openid.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/tests/auth/test_auth_keycloak_openid.py b/src/tests/auth/test_auth_keycloak_openid.py index 23cd43cb..6f6f2e92 100644 --- a/src/tests/auth/test_auth_keycloak_openid.py +++ b/src/tests/auth/test_auth_keycloak_openid.py @@ -18,6 +18,7 @@ access_expiration_duration = 0.5 refresh_expiration_duration = 2.0 +auth_info_ttl = 1.0 class OauthServerMock: @@ -187,7 +188,7 @@ async def test_success_validate_after_refresh(self): self.oauth_server.set_groups('bugy', ['g3']) - await gen.sleep(access_expiration_duration + 0.2) + await gen.sleep(auth_info_ttl + 0.5) valid_1 = await self.authenticator.validate_user(username, mock_request_handler(previous_request=request_1)) self.assertTrue(valid_1) @@ -296,7 +297,7 @@ def create_authenticator(self, dump_file=None): 'client_id': 'my-client', 'secret': 'top_secret', 'group_support': True, - 'auth_info_ttl': 0.4, + 'auth_info_ttl': auth_info_ttl, 'state_dump_file': dump_file }) From 46d7cf3faeb4fa46eb7832c5eef6d986ef323977 Mon Sep 17 00:00:00 2001 From: Thomas Kpenou Date: Thu, 28 May 2026 07:20:44 -0400 Subject: [PATCH 074/148] fix: apply security headers to WebSocket upgrade responses ScriptStreamSocket inherited from tornado.websocket.WebSocketHandler directly, bypassing BaseRequestHandler.set_default_headers(). Adding the override ensures X-Frame-Options, X-Content-Type-Options, Referrer-Policy and Content-Security-Policy are sent on the 101 handshake response, just like all other HTTP endpoints. Tests added for both the API and WebSocket paths. Co-Authored-By: Claude Sonnet 4.6 --- src/tests/web/server_test.py | 32 ++++++++++++++++++++++++++++++++ src/web/server.py | 3 +++ 2 files changed, 35 insertions(+) diff --git a/src/tests/web/server_test.py b/src/tests/web/server_test.py index 65eec05f..d8932ef8 100644 --- a/src/tests/web/server_test.py +++ b/src/tests/web/server_test.py @@ -231,6 +231,38 @@ def test_update_script_config(self): script_content = file_utils.read_file(script_path) self.assertEqual('abcdef', script_content) + # --- Security header tests --- + + def test_security_headers_on_api_response(self): + self.start_server(12345, '127.0.0.1') + response = self._user_session.get('http://127.0.0.1:12345/scripts') + self.assertEqual(200, response.status_code) + self._assert_security_headers(response) + + def test_security_headers_on_websocket_response(self): + self.start_server(12345, '127.0.0.1') + # Plain HTTP GET to the WebSocket endpoint (no Upgrade headers) → Tornado returns 400. + # set_default_headers() is called before the handshake check, so security + # headers must be present in the error response. + response = requests.get('http://127.0.0.1:12345/executions/io/1') + self.assertEqual(400, response.status_code) + self._assert_security_headers(response) + + def _assert_security_headers(self, response): + for header, expected in [ + ('X-Frame-Options', 'DENY'), + ('X-Content-Type-Options', 'nosniff'), + ('Referrer-Policy', 'strict-origin-when-cross-origin'), + ]: + self.assertEqual(expected, response.headers.get(header), + 'Wrong or missing header: ' + header) + + csp = response.headers.get('Content-Security-Policy', '') + self.assertIn("default-src 'self'", csp, 'CSP missing default-src') + self.assertIn("frame-ancestors 'none'", csp, 'CSP missing frame-ancestors') + + # --- end security header tests --- + def test_on_fly_auth(self): self.start_server(12345, '127.0.0.1') diff --git a/src/web/server.py b/src/web/server.py index c662a9bd..d7560aeb 100755 --- a/src/web/server.py +++ b/src/web/server.py @@ -279,6 +279,9 @@ def __init__(self, application, request, **kwargs): self.executor = None + def set_default_headers(self): + _set_security_headers(self) + @check_authorization @inject_user def open(self, user, execution_id): From c4986b17c21316a9c5d44850bc0e4540541f4ada Mon Sep 17 00:00:00 2001 From: Thomas Kpenou Date: Thu, 28 May 2026 07:39:22 -0400 Subject: [PATCH 075/148] test: add security header coverage for static file handler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes point 3 — all three handler types are now covered: - BaseRequestHandler → test_security_headers_on_api_response - BaseStaticHandler → test_security_headers_on_static_response (theme file) - ScriptStreamSocket → test_security_headers_on_websocket_response Co-Authored-By: Claude Sonnet 4.6 --- src/tests/web/server_test.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/tests/web/server_test.py b/src/tests/web/server_test.py index d8932ef8..6750232c 100644 --- a/src/tests/web/server_test.py +++ b/src/tests/web/server_test.py @@ -239,6 +239,19 @@ def test_security_headers_on_api_response(self): self.assertEqual(200, response.status_code) self._assert_security_headers(response) + def test_security_headers_on_static_response(self): + self.start_server(12345, '127.0.0.1') + # Theme files are served by BaseStaticHandler and are accessible without + # authentication (they appear in the allowed_during_login list). + theme_dir = os.path.join(self.conf_folder, 'theme') + os.makedirs(theme_dir, exist_ok=True) + with open(os.path.join(theme_dir, 'style.css'), 'w') as f: + f.write('body { color: red; }') + + response = requests.get('http://127.0.0.1:12345/theme/style.css') + self.assertEqual(200, response.status_code) + self._assert_security_headers(response) + def test_security_headers_on_websocket_response(self): self.start_server(12345, '127.0.0.1') # Plain HTTP GET to the WebSocket endpoint (no Upgrade headers) → Tornado returns 400. From 23cdc5ed74293b8ae58c121f19d335b3da63a25c Mon Sep 17 00:00:00 2001 From: Thomas Kpenou Date: Thu, 28 May 2026 07:46:00 -0400 Subject: [PATCH 076/148] feat: validate date_format and time_format at config load time An invalid format like "DD/MM/YYYY" (no % directives) was previously silently accepted and caused the script to receive a literal "DD/MM/YYYY" string instead of a formatted date. The error is now raised at startup with a clear message pointing to the parameter name and the expected strftime syntax. Co-Authored-By: Claude Sonnet 4.6 --- src/model/parameter_config.py | 10 ++++++++ src/tests/parameter_config_test.py | 38 ++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/model/parameter_config.py b/src/model/parameter_config.py index b078f4f0..2808da88 100644 --- a/src/model/parameter_config.py +++ b/src/model/parameter_config.py @@ -149,6 +149,16 @@ def _validate_config(self): if not self.file_dir: raise Exception('Parameter ' + param_log_name + ' has missing config file_dir') + if self.type == 'date' and self.date_format and '%' not in self.date_format: + raise Exception( + 'Parameter "' + param_log_name + '" has invalid date_format "' + self.date_format + + '": must contain at least one strftime directive (e.g. %Y, %m, %d)') + + if self.type == 'time' and self.time_format and '%' not in self.time_format: + raise Exception( + 'Parameter "' + param_log_name + '" has invalid time_format "' + self.time_format + + '": must contain at least one strftime directive (e.g. %H, %M)') + def str_name(self): names = (name for name in (self.name, self.param, self.description) if name) return next(names, 'unknown') diff --git a/src/tests/parameter_config_test.py b/src/tests/parameter_config_test.py index c03d4a67..9900fcb0 100644 --- a/src/tests/parameter_config_test.py +++ b/src/tests/parameter_config_test.py @@ -254,6 +254,44 @@ def test_map_to_script_time_with_seconds_format(self): parameter_model = create_parameter_model('param1', type='time', time_format='%H:%M:%S') self.assertEqual('14:30:00', parameter_model.map_to_script('14:30')) + # --- date_format / time_format config validation --- + + def test_date_format_valid_custom(self): + # Should not raise — contains % directives + create_parameter_model('param1', type='date', date_format='%d/%m/%Y') + + def test_date_format_invalid_no_directives(self): + self.assertRaisesRegex( + Exception, + 'invalid date_format.*DD/MM/YYYY.*strftime directive', + create_parameter_model, 'param1', type='date', date_format='DD/MM/YYYY') + + def test_date_format_invalid_java_style(self): + self.assertRaisesRegex( + Exception, + 'invalid date_format', + create_parameter_model, 'param1', type='date', date_format='yyyy-MM-dd') + + def test_date_format_validation_only_for_date_type(self): + # Same string on a text parameter should never raise + create_parameter_model('param1', type='text', date_format='DD/MM/YYYY') + + def test_time_format_valid_custom(self): + # Should not raise — contains % directives + create_parameter_model('param1', type='time', time_format='%I:%M %p') + + def test_time_format_invalid_no_directives(self): + self.assertRaisesRegex( + Exception, + 'invalid time_format.*HH:MM.*strftime directive', + create_parameter_model, 'param1', type='time', time_format='HH:MM') + + def test_time_format_validation_only_for_time_type(self): + # Same string on a text parameter should never raise + create_parameter_model('param1', type='text', time_format='HH:MM') + + # --- end format validation tests --- + class TestDefaultValue(unittest.TestCase): From 807278727c8ab1010af4ffb218bebed0dabd20cb Mon Sep 17 00:00:00 2001 From: Thomas Kpenou Date: Thu, 28 May 2026 09:42:58 -0400 Subject: [PATCH 077/148] feat: add HSTS and Permissions-Policy security headers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Permissions-Policy disables camera, microphone and geolocation APIs for every response. Strict-Transport-Security (max-age=31536000; includeSubDomains) is sent only when cookie_secure=True, which signals an HTTPS deployment — same gate already used for the Secure cookie flag. Tests: two new assertions for HSTS presence/absence depending on cookie_secure, and Permissions-Policy added to _assert_security_headers. Co-Authored-By: Claude Sonnet 4.6 --- src/tests/web/server_test.py | 17 +++++++++++++++-- src/web/server.py | 3 +++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/tests/web/server_test.py b/src/tests/web/server_test.py index 6750232c..608c66f3 100644 --- a/src/tests/web/server_test.py +++ b/src/tests/web/server_test.py @@ -261,11 +261,24 @@ def test_security_headers_on_websocket_response(self): self.assertEqual(400, response.status_code) self._assert_security_headers(response) + def test_hsts_present_when_cookie_secure(self): + self.start_server(12345, '127.0.0.1', cookie_secure=True) + response = self._user_session.get('http://127.0.0.1:12345/scripts') + hsts = response.headers.get('Strict-Transport-Security', '') + self.assertIn('max-age=31536000', hsts, 'HSTS header missing when cookie_secure=True') + + def test_hsts_absent_when_not_cookie_secure(self): + self.start_server(12345, '127.0.0.1', cookie_secure=False) + response = self._user_session.get('http://127.0.0.1:12345/scripts') + self.assertNotIn('Strict-Transport-Security', response.headers, + 'HSTS header must not be sent over plain HTTP') + def _assert_security_headers(self, response): for header, expected in [ ('X-Frame-Options', 'DENY'), ('X-Content-Type-Options', 'nosniff'), ('Referrer-Policy', 'strict-origin-when-cross-origin'), + ('Permissions-Policy', 'camera=(), microphone=(), geolocation=()'), ]: self.assertEqual(expected, response.headers.get(header), 'Wrong or missing header: ' + header) @@ -317,13 +330,13 @@ def check_server_running(self): response = self._user_session.get('http://127.0.0.1:12345/conf') self.assertEqual(response.status_code, 200) - def start_server(self, port, address, *, xsrf_protection=XSRF_PROTECTION_TOKEN): + def start_server(self, port, address, *, xsrf_protection=XSRF_PROTECTION_TOKEN, cookie_secure=False): file_download_feature = FileDownloadFeature(UserFileStorage(b'some_secret'), test_utils.temp_folder) config = ServerConfig() config.port = port config.address = address config.xsrf_protection = xsrf_protection - config.cookie_secure = False + config.cookie_secure = cookie_secure config.max_request_size_mb = 1 authorizer = Authorizer(ANY_USER, ['admin_user'], [], ['admin_user'], EmptyGroupProvider()) diff --git a/src/web/server.py b/src/web/server.py index d7560aeb..d793b028 100755 --- a/src/web/server.py +++ b/src/web/server.py @@ -99,6 +99,9 @@ def _set_security_headers(handler): "frame-ancestors 'none'; " "object-src 'none'" ) + handler.set_header('Permissions-Policy', 'camera=(), microphone=(), geolocation=()') + if handler.application.server_config.cookie_secure: + handler.set_header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains') class BaseRequestHandler(tornado.web.RequestHandler): From e45a6d42bc82be928f0d041e358a086e15cecd65 Mon Sep 17 00:00:00 2001 From: Thomas Kpenou Date: Thu, 28 May 2026 10:01:19 -0400 Subject: [PATCH 078/148] feat: add Python 3.13 to CI matrix The `crypt` module was removed in Python 3.13. Fix auth_htpasswd.py to raise InvalidServerConfigException with a migration hint (use bcrypt or SHA-1) instead of crashing with ModuleNotFoundError. The two DES-crypt tests are skipped on Python 3.13+ via @unittest.skipIf since the algorithm is genuinely unavailable without a third-party shim. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ci.yml | 2 +- src/auth/auth_htpasswd.py | 7 ++++++- src/tests/auth/test_auth_htpasswd.py | 3 +++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b68dae8..2c44b9b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.10", "3.11", "3.12"] + python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 diff --git a/src/auth/auth_htpasswd.py b/src/auth/auth_htpasswd.py index ffaa26e5..3b0b443a 100644 --- a/src/auth/auth_htpasswd.py +++ b/src/auth/auth_htpasswd.py @@ -124,7 +124,12 @@ def verify(self, username, password): return hashed_password == expected elif not os_utils.is_win(): - import crypt + try: + import crypt + except ImportError: + raise InvalidServerConfigException( + 'htpasswd contains DES-crypt passwords which are not supported on Python 3.13+. ' + 'Please regenerate passwords using bcrypt (htpasswd -B) or SHA-1 (htpasswd -s).') hashed_password = crypt.crypt(password, existing_password[:2]) return hashed_password == existing_password diff --git a/src/tests/auth/test_auth_htpasswd.py b/src/tests/auth/test_auth_htpasswd.py index 21cdda4c..7e5ecd78 100644 --- a/src/tests/auth/test_auth_htpasswd.py +++ b/src/tests/auth/test_auth_htpasswd.py @@ -1,4 +1,5 @@ import sys +import unittest from unittest import TestCase, mock from unittest.mock import patch @@ -66,6 +67,7 @@ def test_authenticate_success(self): self._assert_authenticated(username, password, authenticator) + @unittest.skipIf(sys.version_info >= (3, 13), 'crypt module removed in Python 3.13+') def test_authenticate_success_when_crypt(self): if self.verifier == 'htpasswd' and os_utils.is_win(): return @@ -109,6 +111,7 @@ def test_authenticate_failure_when_no_user(self): self._assert_rejected('my_user', 'my_pass', authenticator) + @unittest.skipIf(sys.version_info >= (3, 13), 'crypt module removed in Python 3.13+') def test_authenticate_failure_when_crypt_with_plain_password(self): if self.verifier == 'htpasswd' and os_utils.is_win(): return From 6e5b8dbc4300570fbc9c23800c121d63f6638bba Mon Sep 17 00:00:00 2001 From: Thomas Kpenou Date: Thu, 28 May 2026 10:08:58 -0400 Subject: [PATCH 079/148] docs: update README to reflect all improvements since PR #1 - HTTP security headers: add Permissions-Policy and HSTS to the table, note that WebSocket responses are now also covered - date/time types: mention startup validation of date_format/time_format - CI: update matrix to 3.10/3.11/3.12/3.13 - Python 3.13: add breaking-change note on DES-crypt htpasswd passwords Co-Authored-By: Claude Sonnet 4.6 --- README.md | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index c633cfe3..ab829c96 100644 --- a/README.md +++ b/README.md @@ -19,16 +19,19 @@ A new `time` parameter type shows a native time picker in the UI and passes the - `time_format` is a Python [strftime](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) format string. Default: `%H:%M` (24-hour HH:MM). - The UI always shows a native time picker. The script receives the time in the configured format. +- An invalid `time_format` (e.g. `"HH:MM"` instead of `"%H:%M"`) is now detected at startup with a clear error message. ### 2026-05-28 — HTTP security headers -All responses now include the following security headers: +All responses (including WebSocket upgrade responses) now include the following security headers: -| Header | Value | -|--------|-------| -| `X-Content-Type-Options` | `nosniff` | -| `Referrer-Policy` | `strict-origin-when-cross-origin` | -| `Content-Security-Policy` | `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self' ws: wss:; frame-ancestors 'none'; object-src 'none'` | +| Header | Value | Condition | +|--------|-------|-----------| +| `X-Content-Type-Options` | `nosniff` | Always | +| `Referrer-Policy` | `strict-origin-when-cross-origin` | Always | +| `Content-Security-Policy` | `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self' ws: wss:; frame-ancestors 'none'; object-src 'none'` | Always | +| `Permissions-Policy` | `camera=(), microphone=(), geolocation=()` | Always | +| `Strict-Transport-Security` | `max-age=31536000; includeSubDomains` | HTTPS only (`cookie_secure: true`) | `X-Frame-Options: DENY` was already present; `frame-ancestors 'none'` in the CSP provides equivalent coverage for modern browsers. @@ -47,15 +50,18 @@ A new `date` parameter type shows a native date picker in the UI and passes the - `date_format` is a Python [strftime](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) format string. Default: `%Y-%m-%d` (ISO 8601). - The UI always shows a calendar date picker. The script receives the date in the configured format. +- An invalid `date_format` (e.g. `"DD/MM/YYYY"` instead of `"%d/%m/%Y"`) is now detected at startup with a clear error message. ### 2025-05-27 — GitHub Actions CI + secure cookies -- GitHub Actions CI added ([view workflows](https://github.com/knep/script-server/actions)): Python 3.10/3.11/3.12 matrix + Node 22 frontend tests on every push and pull request. +- GitHub Actions CI added ([view workflows](https://github.com/knep/script-server/actions)): Python 3.10/3.11/3.12/3.13 matrix + Node 22 frontend tests on every push and pull request. - Cookies (`username`, `token`, XSRF) are now set with `HttpOnly`, `SameSite=Lax`, and `Secure` flags. The `Secure` flag can be disabled in `conf.json` via `"cookie_secure": false` for HTTP-only deployments. -### 2025-05-27 — Python 3.12 compatibility +### 2025-05-27 — Python 3.12/3.13 compatibility -**Python version support:** updated minimum from Python 3.7 (end-of-life since June 2023) to **Python 3.9+** (Python 3.12 recommended). +**Python version support:** updated minimum from Python 3.7 (end-of-life since June 2023) to **Python 3.9+** (Python 3.13 recommended). + +**Python 3.13 note:** the `crypt` standard-library module was removed in Python 3.13. If your `htpasswd` file contains DES-crypt passwords (entries that do not start with `$2y$`, `$apr1$`, or `{SHA}`), the server will refuse to start with a clear error message. Regenerate those passwords using bcrypt (`htpasswd -B`) or SHA-1 (`htpasswd -s`). **Fixes:** - Replaced invalid string escape sequences (`\d`, `\w`, `\/`, `\ `, `\|`, `\p`, `\[`, `\.`) with raw strings (`r'...'`) in test files — these would become `SyntaxError` in Python 3.14 From 695ee10c37fec24193b9bfcfaa0ef84008227cc0 Mon Sep 17 00:00:00 2001 From: Thomas Kpenou Date: Thu, 28 May 2026 10:24:47 -0400 Subject: [PATCH 080/148] feat: publish Docker image to GitHub Container Registry - Update tools/Dockerfile: Python 3.13-slim, non-root user, copy directly from repo (no pre-build step needed), VOLUME declarations for conf/runners and logs - Add .dockerignore: excludes web-src/, tests, caches and tooling - Add .github/workflows/docker.yml: multi-arch build (amd64 + arm64), push to ghcr.io/knep/script-server on master (latest), stable (stable) and git tags (semver) - Update README Docker section to point to ghcr.io with usage example Co-Authored-By: Claude Sonnet 4.6 --- .dockerignore | 41 ++++++++++++++++++++++++++ .github/workflows/docker.yml | 56 ++++++++++++++++++++++++++++++++++++ README.md | 23 +++++++++++++-- tools/Dockerfile | 30 ++++++++++++++++--- 4 files changed, 144 insertions(+), 6 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/docker.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..3ce2cc82 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,41 @@ +# Version control +.git/ +.github/ + +# Frontend source — pre-built assets are already committed to web/ +web-src/ + +# Dev / build tooling +tools/build.py +tools/deploy_docker.sh +tools/init.py +tools/run_e2e_tests.sh +tools/start_in_virtualenv.sh +samples/ + +# Python cache +__pycache__/ +*.pyc +*.pyo +*.pyd +*.egg-info/ + +# Tests (not needed at runtime) +src/tests/ +src/e2e_tests/ + +# Docs +*.md +LICENSE + +# macOS +.DS_Store + +# IDE / local tooling +.vscode/ +.idea/ +.claude/ + +# Runtime directories (mounted as volumes) +conf/runners/ +logs/ diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 00000000..257bcbce --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,56 @@ +name: Docker + +on: + push: + branches: [ master, stable ] + tags: [ 'v*' ] + +jobs: + build-and-push: + name: Build & push (${{ matrix.platform }}) + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract image metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + tags: | + # master branch → :latest + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }} + # stable branch → :stable + type=raw,value=stable,enable=${{ github.ref == 'refs/heads/stable' }} + # git tag v1.2.3 → :1.2.3 and :1.2 + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: tools/Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/README.md b/README.md index ab829c96..734db8b0 100644 --- a/README.md +++ b/README.md @@ -148,8 +148,27 @@ Internet connection is **not** needed. All the files are loaded from the server. For detailed steps on Linux with virtualenv, see the [Installation guide](https://github.com/bugy/script-server/wiki/Installing-on-virtualenv-(linux)). ##### As a Docker container -Pre-built images are available on [Docker Hub](https://hub.docker.com/r/bugy/script-server/tags). -For usage instructions, see [this ticket](https://github.com/bugy/script-server/issues/171#issuecomment-461620836). + +Images for this fork are published on [GitHub Container Registry](https://github.com/knep/script-server/pkgs/container/script-server): + +```bash +# Pull the latest image (built from master) +docker pull ghcr.io/knep/script-server:latest + +# Run with your script configs and logs persisted +docker run -d \ + -p 5000:5000 \ + -v /path/to/your/conf/runners:/app/conf/runners \ + -v /path/to/your/logs:/app/logs \ + ghcr.io/knep/script-server:latest +``` + +Available tags: +| Tag | Source | +|-----|--------| +| `latest` | `master` branch — most recent changes | +| `stable` | `stable` branch | +| `1.2.3` / `1.2` | Git tag `v1.2.3` | ### For development 1. Clone this repository diff --git a/tools/Dockerfile b/tools/Dockerfile index d1c8fd8a..7e5cecdc 100644 --- a/tools/Dockerfile +++ b/tools/Dockerfile @@ -1,8 +1,30 @@ -FROM python:3.9-slim +FROM python:3.13-slim + +# Create a non-root user +RUN useradd --create-home --shell /bin/bash scriptserver -COPY build/script-server /app WORKDIR /app -RUN pip install -r requirements.txt + +# Install Python dependencies first for better layer caching +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code (web/ contains pre-built frontend assets) +COPY src/ ./src/ +COPY web/ ./web/ +COPY conf/ ./conf/ +COPY launcher.py ./ + +# Ensure writable directories exist and belong to the app user +RUN mkdir -p conf/runners logs \ + && chown -R scriptserver:scriptserver /app + +USER scriptserver + +# conf/runners — mount your script configs here +# logs/ — execution logs written here +VOLUME ["/app/conf/runners", "/app/logs"] EXPOSE 5000 -CMD [ "python3", "launcher.py" ] \ No newline at end of file + +CMD ["python3", "launcher.py"] From d3eb85706793a6390d907f0b71509505a62dbd5b Mon Sep 17 00:00:00 2001 From: Thomas Kpenou Date: Thu, 28 May 2026 10:42:35 -0400 Subject: [PATCH 081/148] docs: announce Docker image in What's new section Co-Authored-By: Claude Sonnet 4.6 --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 734db8b0..b32162ba 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,21 @@ ## What's new in this fork +### 2026-05-28 — Docker image on GitHub Container Registry + +A Docker image for this fork is now published automatically on every commit to `master`: + +```bash +docker run -d \ + -p 5000:5000 \ + -v /path/to/your/conf/runners:/app/conf/runners \ + -v /path/to/your/logs:/app/logs \ + ghcr.io/knep/script-server:latest +``` + +Available tags: `latest` (master), `stable`, and semver tags (e.g. `1.19.0`) on git releases. +See the full [installation instructions](#as-a-docker-container) below. + ### 2026-05-28 — New `time` parameter type A new `time` parameter type shows a native time picker in the UI and passes the selected time to the script in a configurable format. From b6e439df019f5ae826e080cc70fccdb8a1ff7b0f Mon Sep 17 00:00:00 2001 From: Thomas Kpenou Date: Thu, 28 May 2026 10:49:05 -0400 Subject: [PATCH 082/148] fix: build frontend before Docker image web/ is gitignored (it's a build artifact). Add a Node 22 + npm ci + npm run build step in the workflow so web/ exists in the build context before docker buildx runs. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/docker.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 257bcbce..e0650feb 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -16,6 +16,20 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + cache-dependency-path: web-src/package-lock.json + + - name: Build frontend + working-directory: web-src + env: + NODE_OPTIONS: --openssl-legacy-provider + run: npm ci && npm run build + # Outputs to ../web/ (see web-src/vue.config.js outputDir) + - name: Set up QEMU uses: docker/setup-qemu-action@v3 From c1cddf74954019dabac4d46a776a9e5b54e2499b Mon Sep 17 00:00:00 2001 From: Thomas Kpenou Date: Thu, 28 May 2026 10:56:54 -0400 Subject: [PATCH 083/148] feat: add docker-compose.yml Minimal setup: mounts conf/runners and logs. Optional volumes for conf.json, .htpasswd and custom theme are included as comments. Co-Authored-By: Claude Sonnet 4.6 --- docker-compose.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..9e08372a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +services: + script-server: + image: ghcr.io/knep/script-server:latest + container_name: script-server + restart: unless-stopped + ports: + - "5000:5000" + volumes: + # Script configurations (required) — put your .json runner files here + - ./conf/runners:/app/conf/runners + # Execution logs + - ./logs:/app/logs + # Optional: server configuration (auth, SSL, port, etc.) + # - ./conf/conf.json:/app/conf/conf.json:ro + # Optional: htpasswd file for basic auth + # - ./conf/.htpasswd:/app/conf/.htpasswd:ro + # Optional: custom theme (CSS/images) + # - ./conf/theme:/app/conf/theme:ro From c840937790c1e1657dc16b4ec7df75979386820a Mon Sep 17 00:00:00 2001 From: Thomas Kpenou Date: Thu, 28 May 2026 11:29:38 -0400 Subject: [PATCH 084/148] fix: upgrade GitHub Actions to Node.js 24 compatible versions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump all GitHub Actions to their latest major versions (Node.js 20 → 24 runtime): checkout@v6, setup-python@v6, setup-node@v6, docker/*@v4+, metadata-action@v6, build-push-action@v7. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ci.yml | 8 ++++---- .github/workflows/docker.yml | 26 ++++++++++++++++++++------ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c44b9b5..71fe900c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,10 +16,10 @@ jobs: python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} cache: pip @@ -40,10 +40,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: "22" cache: npm diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 257bcbce..4cdfdda6 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -14,16 +14,30 @@ jobs: packages: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: "22" + cache: npm + cache-dependency-path: web-src/package-lock.json + + - name: Build frontend + working-directory: web-src + env: + NODE_OPTIONS: --openssl-legacy-provider + run: npm ci && npm run build + # Outputs to ../web/ (see web-src/vue.config.js outputDir) - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} @@ -31,7 +45,7 @@ jobs: - name: Extract image metadata id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: ghcr.io/${{ github.repository }} tags: | @@ -44,7 +58,7 @@ jobs: type=semver,pattern={{major}}.{{minor}} - name: Build and push - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . file: tools/Dockerfile From 3180c1a123e8f57a732272cf192921f157d45fed Mon Sep 17 00:00:00 2001 From: Thomas Kpenou Date: Thu, 28 May 2026 11:57:19 -0400 Subject: [PATCH 085/148] test: add unit tests for TimeField and DateField components 42 tests covering config (label, input type, required, disabled), values (initial display, external update, user change via @change event), and validation (required/empty, null, disabled override, data-error attribute). Note: tests use element.value + trigger('change') instead of setValue() because Vue re-renders between the synthetic input/change events in setValue and resets the bound :value, preventing the component from reading the new value. Co-Authored-By: Claude Sonnet 4.6 --- .../components/inputs/DateField_test.js | 180 +++++++++++++++++ .../components/inputs/TimeField_test.js | 181 ++++++++++++++++++ 2 files changed, 361 insertions(+) create mode 100644 web-src/tests/unit/common/components/inputs/DateField_test.js create mode 100644 web-src/tests/unit/common/components/inputs/TimeField_test.js diff --git a/web-src/tests/unit/common/components/inputs/DateField_test.js b/web-src/tests/unit/common/components/inputs/DateField_test.js new file mode 100644 index 00000000..5326aed7 --- /dev/null +++ b/web-src/tests/unit/common/components/inputs/DateField_test.js @@ -0,0 +1,180 @@ +'use strict'; + +import {mount} from '@vue/test-utils'; +import DateField from '@/common/components/inputs/DateField'; +import {setDeepProp, vueTicks, wrapVModel} from '../../../test_utils'; + +describe('Test DateField', function () { + + let datefield; + + beforeEach(async function () { + datefield = mount(DateField, { + propsData: { + config: { + name: 'Start date', + required: false, + description: 'Pick a start date' + }, + value: '2025-05-27' + } + }); + wrapVModel(datefield); + await vueTicks(); + }); + + describe('Test config', function () { + + it('Test label displays config.name', function () { + expect(datefield.find('label').text()).toBe('Start date'); + }); + + it('Test input type is date', function () { + expect(datefield.find('input').element.type).toBe('date'); + }); + + it('Test input is not required by default', function () { + expect(datefield.find('input').element.required).toBe(false); + }); + + it('Test input becomes required when config.required is set', async function () { + setDeepProp(datefield, 'config.required', true); + await vueTicks(); + + expect(datefield.find('input').element.required).toBe(true); + }); + + it('Test input is enabled by default', function () { + expect(datefield.find('input').element.disabled).toBe(false); + }); + + it('Test input is disabled when disabled prop is true', async function () { + datefield.setProps({disabled: true}); + await vueTicks(); + + expect(datefield.find('input').element.disabled).toBe(true); + }); + + it('Test label has active class when value is set', function () { + expect(datefield.find('label').classes()).toContain('active'); + }); + + it('Test label has no active class when value is empty', async function () { + datefield.setProps({value: ''}); + await vueTicks(); + + expect(datefield.find('label').classes()).not.toContain('active'); + }); + + }); + + describe('Test values', function () { + + it('Test displays initial value', function () { + expect(datefield.find('input').element.value).toBe('2025-05-27'); + expect(datefield.vm.value).toBe('2025-05-27'); + }); + + it('Test external value change updates input', async function () { + datefield.setProps({value: '2026-01-01'}); + await vueTicks(); + + expect(datefield.find('input').element.value).toBe('2026-01-01'); + expect(datefield.vm.value).toBe('2026-01-01'); + }); + + it('Test user change emits input event with new value', async function () { + const input = datefield.find('input'); + input.element.value = '2026-06-15'; + await input.trigger('change'); + await vueTicks(); + + expect(datefield.vm.value).toBe('2026-06-15'); + }); + + it('Test user change to another valid date', async function () { + const input = datefield.find('input'); + input.element.value = '2025-12-31'; + await input.trigger('change'); + await vueTicks(); + + expect(datefield.vm.value).toBe('2025-12-31'); + }); + + }); + + describe('Test validation', function () { + + it('Test no error for valid initial value', function () { + expect(datefield.vm.error).toBe(''); + }); + + it('Test no error when not required and value becomes empty', async function () { + datefield.setProps({value: ''}); + await vueTicks(); + + expect(datefield.currentError).toBe(''); + }); + + it('Test required error when required and value is empty string', async function () { + setDeepProp(datefield, 'config.required', true); + datefield.setProps({value: ''}); + await vueTicks(); + + expect(datefield.currentError).toBe('required'); + }); + + it('Test required error when required and value is null', async function () { + setDeepProp(datefield, 'config.required', true); + datefield.setProps({value: null}); + await vueTicks(); + + expect(datefield.currentError).toBe('required'); + }); + + it('Test no error when required and value is present', async function () { + setDeepProp(datefield, 'config.required', true); + datefield.setProps({value: '2025-09-01'}); + await vueTicks(); + + expect(datefield.currentError).toBe(''); + }); + + it('Test error clears when value provided after being empty', async function () { + setDeepProp(datefield, 'config.required', true); + datefield.setProps({value: ''}); + await vueTicks(); + expect(datefield.currentError).toBe('required'); + + datefield.setProps({value: '2025-12-01'}); + await vueTicks(); + expect(datefield.currentError).toBe(''); + }); + + it('Test no error when disabled even if required and empty', async function () { + datefield.setProps({disabled: true}); + setDeepProp(datefield, 'config.required', true); + datefield.setProps({value: ''}); + await vueTicks(); + + expect(datefield.currentError).toBe(''); + }); + + it('Test data-error attribute reflects current error', async function () { + setDeepProp(datefield, 'config.required', true); + datefield.setProps({value: ''}); + await vueTicks(); + + expect(datefield.find('div').attributes('data-error')).toBe('required'); + }); + + it('Test data-error attribute is empty when no error', async function () { + datefield.setProps({value: '2025-05-27'}); + await vueTicks(); + + expect(datefield.find('div').attributes('data-error')).toBe(''); + }); + + }); + +}); diff --git a/web-src/tests/unit/common/components/inputs/TimeField_test.js b/web-src/tests/unit/common/components/inputs/TimeField_test.js new file mode 100644 index 00000000..5d1bb3a4 --- /dev/null +++ b/web-src/tests/unit/common/components/inputs/TimeField_test.js @@ -0,0 +1,181 @@ +'use strict'; + +import {mount} from '@vue/test-utils'; +import TimeField from '@/common/components/inputs/TimeField'; +import {setDeepProp, vueTicks, wrapVModel} from '../../../test_utils'; + +describe('Test TimeField', function () { + + let timefield; + + beforeEach(async function () { + timefield = mount(TimeField, { + propsData: { + config: { + name: 'Start time', + required: false, + description: 'Pick a start time' + }, + value: '10:00' + } + }); + wrapVModel(timefield); + await vueTicks(); + }); + + describe('Test config', function () { + + it('Test label displays config.name', function () { + expect(timefield.find('label').text()).toBe('Start time'); + }); + + it('Test input type is time', function () { + expect(timefield.find('input').element.type).toBe('time'); + }); + + it('Test input is not required by default', function () { + expect(timefield.find('input').element.required).toBe(false); + }); + + it('Test input becomes required when config.required is set', async function () { + setDeepProp(timefield, 'config.required', true); + await vueTicks(); + + expect(timefield.find('input').element.required).toBe(true); + }); + + it('Test input is enabled by default', function () { + expect(timefield.find('input').element.disabled).toBe(false); + }); + + it('Test input is disabled when disabled prop is true', async function () { + timefield.setProps({disabled: true}); + await vueTicks(); + + expect(timefield.find('input').element.disabled).toBe(true); + }); + + it('Test label has active class when value is set', function () { + expect(timefield.find('label').classes()).toContain('active'); + }); + + it('Test label has no active class when value is empty', async function () { + timefield.setProps({value: ''}); + await vueTicks(); + + expect(timefield.find('label').classes()).not.toContain('active'); + }); + + }); + + describe('Test values', function () { + + it('Test displays initial value', function () { + expect(timefield.find('input').element.value).toBe('10:00'); + expect(timefield.vm.value).toBe('10:00'); + }); + + it('Test external value change updates input', async function () { + timefield.setProps({value: '14:30'}); + await vueTicks(); + + expect(timefield.find('input').element.value).toBe('14:30'); + expect(timefield.vm.value).toBe('14:30'); + }); + + it('Test user change emits input event with new value', async function () { + const input = timefield.find('input'); + input.element.value = '08:15'; + await input.trigger('change'); + await vueTicks(); + + expect(timefield.vm.value).toBe('08:15'); + }); + + it('Test user change to another valid time', async function () { + const input = timefield.find('input'); + input.element.value = '23:59'; + await input.trigger('change'); + await vueTicks(); + + expect(timefield.vm.value).toBe('23:59'); + }); + + }); + + describe('Test validation', function () { + + it('Test no error for valid initial value', function () { + expect(timefield.vm.error).toBe(''); + }); + + it('Test no error when not required and value becomes empty', async function () { + timefield.setProps({value: ''}); + await vueTicks(); + + expect(timefield.currentError).toBe(''); + }); + + it('Test required error when required and value is empty string', async function () { + setDeepProp(timefield, 'config.required', true); + timefield.setProps({value: ''}); + await vueTicks(); + + expect(timefield.currentError).toBe('required'); + }); + + it('Test required error when required and value is null', async function () { + setDeepProp(timefield, 'config.required', true); + timefield.setProps({value: null}); + await vueTicks(); + + expect(timefield.currentError).toBe('required'); + }); + + it('Test no error when required and value is present', async function () { + setDeepProp(timefield, 'config.required', true); + timefield.setProps({value: '09:00'}); + await vueTicks(); + + expect(timefield.currentError).toBe(''); + }); + + it('Test error clears when value provided after being empty', async function () { + setDeepProp(timefield, 'config.required', true); + timefield.setProps({value: ''}); + await vueTicks(); + expect(timefield.currentError).toBe('required'); + + timefield.setProps({value: '12:00'}); + await vueTicks(); + expect(timefield.currentError).toBe(''); + }); + + it('Test no error when disabled even if required and empty', async function () { + timefield.setProps({disabled: true}); + setDeepProp(timefield, 'config.required', true); + timefield.setProps({value: ''}); + await vueTicks(); + + expect(timefield.currentError).toBe(''); + }); + + it('Test data-error attribute reflects current error', async function () { + setDeepProp(timefield, 'config.required', true); + timefield.setProps({value: ''}); + await vueTicks(); + + expect(timefield.find('div').attributes('data-error')).toBe('required'); + }); + + it('Test data-error attribute is empty when no error', async function () { + await vueTicks(); + timefield.setProps({value: '10:00'}); + await vueTicks(); + + expect(timefield.find('div').attributes('data-error')).toBe(''); + }); + + }); + +}); From 2b7fd0c5460fca9dc684dd281c37433e86ae8f3a Mon Sep 17 00:00:00 2001 From: Thomas Kpenou Date: Thu, 28 May 2026 12:11:54 -0400 Subject: [PATCH 086/148] docs: update README with docker-compose and test coverage improvements Add What's new entries for docker-compose.yml (PR #9) and frontend unit tests for DateField/TimeField (PR #11). Also expand the Docker installation section with a docker-compose usage example and volume customisation hints. Co-Authored-By: Claude Sonnet 4.6 --- README.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/README.md b/README.md index b32162ba..27ce0e99 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,25 @@ ## What's new in this fork +### 2026-05-28 — docker-compose.yml for easy deployment + +A `docker-compose.yml` is now included at the root of the repository for quick local deployments: + +```bash +# Start +docker compose up -d + +# Stop +docker compose down +``` + +Mount your script configs in `./conf/runners/` and logs will be written to `./logs/`. +See the full [docker-compose instructions](#with-docker-compose) below. + +### 2026-05-28 — Frontend unit tests for `date` and `time` components + +Unit tests added for the `DateField` and `TimeField` Vue components (42 tests total), covering label/input rendering, two-way value binding, and required-field validation. + ### 2026-05-28 — Docker image on GitHub Container Registry A Docker image for this fork is now published automatically on every commit to `master`: @@ -185,6 +204,29 @@ Available tags: | `stable` | `stable` branch | | `1.2.3` / `1.2` | Git tag `v1.2.3` | +##### With docker-compose + +A ready-to-use `docker-compose.yml` is included at the root of the repository: + +```bash +# Clone or download docker-compose.yml, then: +docker compose up -d +``` + +Place your script runner configs in `./conf/runners/` (created automatically on first run). +Execution logs are written to `./logs/`. + +To customise the server (auth, SSL, port…), uncomment the optional volume lines in `docker-compose.yml`: + +```yaml +volumes: + - ./conf/runners:/app/conf/runners + - ./logs:/app/logs + # - ./conf/conf.json:/app/conf/conf.json:ro # server config + # - ./conf/.htpasswd:/app/conf/.htpasswd:ro # htpasswd auth + # - ./conf/theme:/app/conf/theme:ro # custom CSS/images +``` + ### For development 1. Clone this repository 2. Run `tools/init.py --no-npm` From cf7aad66440f3143032ffe054bb1598732bff3c4 Mon Sep 17 00:00:00 2001 From: Thomas Kpenou Date: Thu, 28 May 2026 12:22:03 -0400 Subject: [PATCH 087/148] fix: prevent flaky test_time_buffer_aggregated_read_until_closed on Python 3.10 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test used time_buffered(100ms) while _test_read_until_closed sends late messages after a 100ms sleep. Both timers expire simultaneously, creating a race: the flush thread can process the late messages and close the buffered observable before read_until_closed subscribes (at t≈140ms), producing data=[]. Switch to 30ms so flush cycles land at t=30/60/90/120ms; the subscription is established at t≈49ms and the late-message flush happens at t≈120ms, after the subscriber is in place. Co-Authored-By: Claude Sonnet 4.6 --- src/tests/observable_test.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/tests/observable_test.py b/src/tests/observable_test.py index 3c5a8897..9f1a5367 100644 --- a/src/tests/observable_test.py +++ b/src/tests/observable_test.py @@ -745,7 +745,14 @@ def test_time_buffer_read_until_closed(self): def test_time_buffer_aggregated_read_until_closed(self): observable = self.create_observable() - buffered_observable = observable.time_buffered(100, lambda chunks: ['|'.join(chunks)]) + # Use 30ms period (not 100ms) to avoid a timing race: the helper + # _test_read_until_closed sends late messages after 100ms sleep. + # With period=100ms both the flush thread and the async thread fire at + # ~t=100ms; the flush thread can close the buffered observable before + # read_until_closed subscribes (at t≈140ms), resulting in empty data. + # With period=30ms flush cycles land at t=30/60/90/120ms; the subscriber + # is set up at t≈49ms and the late-message flush happens at t≈120ms. + buffered_observable = observable.time_buffered(30, lambda chunks: ['|'.join(chunks)]) data, _, late_messages = self._test_read_until_closed( observable, From 8c742ab489b4737e4b741d78a3c87d8b22ca7840 Mon Sep 17 00:00:00 2001 From: Thomas Kpenou Date: Thu, 28 May 2026 14:23:16 -0400 Subject: [PATCH 088/148] feat(vue3/phase1): replace Vue CLI + Karma with Vite + Vitest, upgrade to Vue 3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build tool: - Remove @vue/cli-service, karma, mocha and all webpack-based tooling - Add vite@5, @vitejs/plugin-vue, vitest@2, jsdom - Create vite.config.js (multi-page: index/admin/login, SCSS globals, dev proxy) - Move HTML entry points from public/ to web-src/ root with + + diff --git a/web-src/index.html b/web-src/index.html new file mode 100644 index 00000000..61f05155 --- /dev/null +++ b/web-src/index.html @@ -0,0 +1,18 @@ + + + + + Script server + + + + + + + + +
+ + + + diff --git a/web-src/login.html b/web-src/login.html new file mode 100644 index 00000000..a5cc13bd --- /dev/null +++ b/web-src/login.html @@ -0,0 +1,78 @@ + + + + + Script server + + + + + + + + +
+
+
+
+
+ + + + + + + + + + + + + + + + + diff --git a/web-src/package.json b/web-src/package.json index a356a035..1c3cfce4 100644 --- a/web-src/package.json +++ b/web-src/package.json @@ -2,6 +2,7 @@ "name": "script-server", "version": "1.18.0", "private": true, + "type": "module", "dependencies": { "ace-builds": "^1.11.2", "axios": "^1.7.7", @@ -9,57 +10,36 @@ "codemirror": "^5.65.9", "core-js": "^3.25.3", "dompurify": "^2.5.4", + "lodash": "^4.18.1", "marked": "^4.1.0", "material-design-icons": "^3.0.1", "materialize-css": "git+https://github.com/bugy/materialize.git#90803fb", "tinycolor2": "^1.4.2", "typeface-roboto": "^1.1.13", - "vue": "^2.7.10", - "vue-router": "^3.6.5", - "vuex": "^3.6.2" + "vue": "^3.4.0", + "vue-router": "^4.3.0", + "vuex": "^4.1.0" }, "devDependencies": { - "@babel/plugin-proposal-optional-chaining": "^7.18.9", - "@stryker-mutator/core": "^6.4.2", - "@stryker-mutator/javascript-mutator": "^4.0.0", - "@stryker-mutator/karma-runner": "^6.4.2", - "@stryker-mutator/webpack-transpiler": "^4.0.0", - "@testing-library/jest-dom": "^5.16.5", - "@vue/cli": "^4.5.19", - "@vue/cli-plugin-babel": "^4.5.19", - "@vue/cli-plugin-router": "^4.5.19", - "@vue/cli-plugin-vuex": "^4.5.19", - "@vue/cli-service": "^4.5.19", - "@vue/test-utils": "1.3.0", + "@testing-library/jest-dom": "^6.4.0", + "@vitejs/plugin-vue": "^5.1.0", + "@vue/test-utils": "^2.4.6", "axios-mock-adapter": "^1.21.2", - "babel-plugin-rewire": "^1.2.0", - "expect": "^25.5.0", - "exports-loader": "^1.1.1", - "http-proxy-middleware": "^1.3.1", - "jest-extended": "^0.11.5", + "jest-extended": "^4.0.2", "jquery": "^3.6.1", - "karma": "^6.4.1", - "karma-allure-reporter": "^1.4.6", - "karma-chrome-launcher": "^3.1.1", - "karma-firefox-launcher": "^2.1.2", - "karma-mocha": "^2.0.1", - "karma-sourcemap-loader": "^0.3.8", - "karma-spec-reporter": "^0.0.34", - "karma-webpack": "^4.0.2", - "mocha": "^8.4.0", + "jsdom": "^24.0.0", "mock-socket": "^9.1.5", "sass": "^1.55.0", - "sass-loader": "^10.3.1", - "sinon": "^7.5.0", - "vue-cli-plugin-unit-karmajs": "git+https://git@github.com/bugy/vue-cli-plugin-unit-karmajs.git", - "vue-template-compiler": "^2.7.10" + "sinon": "^17.0.0", + "vite": "^5.4.0", + "vitest": "^2.1.0" }, "scripts": { - "serve": "vue-cli-service serve", - "build": "vue-cli-service build", - "test:unit": "vue-cli-service test:unit", - "test:unit-ci": "vue-cli-service test:unit -b ChromeHeadless", - "stryker": "stryker run tests/unit/stryker.conf.js" + "serve": "vite", + "build": "vite build", + "preview": "vite preview", + "test:unit": "vitest", + "test:unit-ci": "vitest run" }, "browserslist": [ "> 1%", diff --git a/web-src/src/admin/admin.js b/web-src/src/admin/admin.js index 7564d96c..53c06a96 100644 --- a/web-src/src/admin/admin.js +++ b/web-src/src/admin/admin.js @@ -1,18 +1,16 @@ import '@/common/materializecss/imports/tabs' import '@/common/style_imports'; -import Vue from 'vue'; +import {createApp} from 'vue'; import AdminApp from './AdminApp'; -import './AdminApp'; import router from './router/router'; import vueDirectives from '@/common/vueDirectives' import {forEachKeyValue} from '@/common/utils/common' +const app = createApp(AdminApp) + forEachKeyValue(vueDirectives, (id, definition) => { - Vue.directive(id, definition) + app.directive(id, definition) }) -//noinspection JSAnnotator -new Vue({ - router, - render: h => h(AdminApp) -}).$mount('#admin-page'); +app.use(router) +app.mount('#admin-page') diff --git a/web-src/src/admin/router/router.js b/web-src/src/admin/router/router.js index 9cfc5d9a..00c5a288 100644 --- a/web-src/src/admin/router/router.js +++ b/web-src/src/admin/router/router.js @@ -1,15 +1,12 @@ import {routerChildren as executionRouterChildren} from '@/common/components/history/executions-log-page'; -import Vue from 'vue'; -import VueRouter from 'vue-router'; +import {createRouter, createWebHashHistory} from 'vue-router'; import AdminExecutionsLogPage from '../components/history/AdminExecutionsLogPage'; import ScriptConfig from '../components/scripts-config/ScriptConfig'; import ScriptConfigListPage from '../components/scripts-config/ScriptConfigListPage'; import ScriptsList from '../components/scripts-config/ScriptsList'; -Vue.use(VueRouter); - -const router = new VueRouter({ - mode: 'hash', +const router = createRouter({ + history: createWebHashHistory(), routes: [ { path: '/logs', @@ -24,9 +21,10 @@ const router = new VueRouter({ {path: ':scriptName', component: ScriptConfig, props: true} ] }, - {path: '*', redirect: '/logs'} + // Vue Router 4: wildcard must use :pathMatch(.*)* syntax + {path: '/:pathMatch(.*)*', redirect: '/logs'} ], linkActiveClass: 'active' }); -export default router \ No newline at end of file +export default router diff --git a/web-src/src/common/vueDirectives.js b/web-src/src/common/vueDirectives.js index 4bfab6c6..ce83d312 100644 --- a/web-src/src/common/vueDirectives.js +++ b/web-src/src/common/vueDirectives.js @@ -1,8 +1,11 @@ import {trimTextNodes} from '@/common/utils/common' +// Vue 3 directive hook names: +// inserted → beforeMount +// componentUpdated → updated export default { 'trim-text': { - inserted: trimTextNodes, - componentUpdated: trimTextNodes + beforeMount: trimTextNodes, + updated: trimTextNodes } -} \ No newline at end of file +} diff --git a/web-src/src/main-app/index.js b/web-src/src/main-app/index.js index 6ae4c898..6d1ca11a 100644 --- a/web-src/src/main-app/index.js +++ b/web-src/src/main-app/index.js @@ -1,19 +1,17 @@ import '@/common/style_imports' import {forEachKeyValue} from '@/common/utils/common'; -import Vue from 'vue' +import {createApp} from 'vue' import MainApp from './MainApp.vue'; import router from './router/router' import store from './store' import vueDirectives from '@/common/vueDirectives' -Vue.config.productionTip = false; +const app = createApp(MainApp) forEachKeyValue(vueDirectives, (id, definition) => { - Vue.directive(id, definition) + app.directive(id, definition) }) -new Vue({ - router, - store, - render: h => h(MainApp) -}).$mount('#app'); \ No newline at end of file +app.use(router) +app.use(store) +app.mount('#app') diff --git a/web-src/src/main-app/router/router.js b/web-src/src/main-app/router/router.js index 65dd939e..dd012d90 100644 --- a/web-src/src/main-app/router/router.js +++ b/web-src/src/main-app/router/router.js @@ -1,16 +1,13 @@ import {routerChildren as executionRouterChildren} from '@/common/components/history/executions-log-page'; -import Vue from 'vue'; -import VueRouter from 'vue-router'; +import {createRouter, createWebHashHistory} from 'vue-router'; import AppWelcomePanel from '../components/AppWelcomePanel'; import AppHistoryHeader from '../components/history/AppHistoryHeader'; import AppHistoryPanel from '../components/history/AppHistoryPanel'; import MainAppContent from '../components/scripts/MainAppContent'; import ScriptHeader from '../components/scripts/ScriptHeader'; -Vue.use(VueRouter); - -const router = new VueRouter({ - mode: 'hash', +const router = createRouter({ + history: createWebHashHistory(), routes: [ { path: '/history', @@ -35,4 +32,4 @@ const router = new VueRouter({ ] }); -export default router \ No newline at end of file +export default router diff --git a/web-src/src/main-app/store/index.js b/web-src/src/main-app/store/index.js index ba390b55..bed66e52 100644 --- a/web-src/src/main-app/store/index.js +++ b/web-src/src/main-app/store/index.js @@ -1,8 +1,7 @@ import historyModule from '@/common/store/executions-module'; import {isNull, logError} from '@/common/utils/common'; import get from 'lodash/get'; -import Vue from 'vue' -import Vuex from 'vuex' +import {createStore} from 'vuex' import authModule from '@/common/store/auth'; import scheduleModule from './scriptSchedule'; import pageModule from './page'; @@ -15,9 +14,7 @@ import serverConfigModule from './serverConfig'; import {axiosInstance} from '@/common/utils/axios_utils'; -Vue.use(Vuex); - -const store = new Vuex.Store({ +const store = createStore({ modules: { scripts: scriptsModule, serverConfig: serverConfigModule, @@ -53,7 +50,7 @@ const store = new Vuex.Store({ }, mutations: {}, - strict: process.env.NODE_ENV !== 'production' + strict: import.meta.env.MODE !== 'production' }); store.watch((state) => state.scripts.selectedScript, (selectedScript) => { @@ -96,4 +93,4 @@ window.addEventListener('beforeunload', function (e) { } }); -export default store; \ No newline at end of file +export default store; diff --git a/web-src/tests/unit/setup.js b/web-src/tests/unit/setup.js new file mode 100644 index 00000000..58515274 --- /dev/null +++ b/web-src/tests/unit/setup.js @@ -0,0 +1,22 @@ +/** + * Vitest global setup — replaces the Karma/Mocha tests/unit/index.js entry. + * + * - Extends `expect` with jest-dom and jest-extended matchers + * - Makes jQuery available as window.$ (required by materialize-css init code) + * - Registers @vue/test-utils auto-destroy after each test + */ +import {expect, afterEach} from 'vitest' +import * as domMatchers from '@testing-library/jest-dom/matchers' +import jestExtended from 'jest-extended' +import $ from 'jquery' +import {enableAutoUnmount} from '@vue/test-utils' + +expect.extend(domMatchers) +expect.extend(jestExtended) + +// Make jQuery globally available (materialize-css and some components rely on it) +globalThis.$ = $ +globalThis.jQuery = $ + +// Auto-unmount Vue wrappers after every test (Vue 3 equivalent of enableAutoDestroy) +enableAutoUnmount(afterEach) diff --git a/web-src/vite.config.js b/web-src/vite.config.js new file mode 100644 index 00000000..4e4ba02c --- /dev/null +++ b/web-src/vite.config.js @@ -0,0 +1,69 @@ +import {defineConfig} from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import {fileURLToPath, URL} from 'node:url' + +export default defineConfig({ + plugins: [vue()], + + // Relative public path — equivalent to vue.config.js publicPath: '' + base: '', + + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + }, + // Allow imports without explicit .vue extension (matches webpack behaviour) + extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'] + }, + + css: { + preprocessorOptions: { + scss: { + // Make project variables and materialize tokens available in every .scss / diff --git a/web-src/src/admin/components/scripts-config/ScriptConfig.vue b/web-src/src/admin/components/scripts-config/ScriptConfig.vue index 876378ba..93ef32f5 100644 --- a/web-src/src/admin/components/scripts-config/ScriptConfig.vue +++ b/web-src/src/admin/components/scripts-config/ScriptConfig.vue @@ -75,7 +75,7 @@ export default { height: 100%; } -.script-config >>> h5 { +.script-config :deep(h5) { margin-left: 0.75rem; margin-top: 0.5rem; margin-bottom: 2rem; @@ -100,7 +100,7 @@ footer.page-footer { display: flex; } -.script-config >>> footer.page-footer a.btn-flat { +.script-config :deep(footer.page-footer a.btn-flat) { height: 48px; line-height: 48px; width: 136px; @@ -113,16 +113,16 @@ footer.page-footer { flex: 1 1 0; } -.script-config >>> footer.page-footer .preloader-wrapper { +.script-config :deep(footer.page-footer .preloader-wrapper) { width: 30px; height: 30px; } -.script-config >>> footer.page-footer .spinner-layer { +.script-config :deep(footer.page-footer .spinner-layer) { border: var(--font-on-primary-color-dark-main); } -.script-config >>> footer.page-footer .btn-flat i { +.script-config :deep(footer.page-footer .btn-flat i) { font-size: 24px; } diff --git a/web-src/src/admin/components/scripts-config/ScriptParamList.vue b/web-src/src/admin/components/scripts-config/ScriptParamList.vue index c4b9d936..60df7614 100644 --- a/web-src/src/admin/components/scripts-config/ScriptParamList.vue +++ b/web-src/src/admin/components/scripts-config/ScriptParamList.vue @@ -175,7 +175,7 @@ export default { \ No newline at end of file diff --git a/web-src/src/common/components/log_panel.vue b/web-src/src/common/components/log_panel.vue index 401b07b1..1b7b59cd 100644 --- a/web-src/src/common/components/log_panel.vue +++ b/web-src/src/common/components/log_panel.vue @@ -235,7 +235,7 @@ export default { box-shadow: 0 -7px 8px -4px rgba(0, 0, 0, 0.4) inset; } -.log-panel >>> .log-content.terminal-output img { +.log-panel :deep(.log-content.terminal-output img) { max-width: 100% } @@ -260,7 +260,7 @@ export default { } /*noinspection CssInvalidPropertyValue,CssOverwrittenProperties*/ -.log-panel >>> .log-content { +.log-panel :deep(.log-content) { display: block; overflow-y: auto; height: 100%; diff --git a/web-src/src/common/materializecss/imports/global.js b/web-src/src/common/materializecss/imports/global.js index b4ba971b..888706e0 100644 --- a/web-src/src/common/materializecss/imports/global.js +++ b/web-src/src/common/materializecss/imports/global.js @@ -1,6 +1,9 @@ import 'materialize-css/js/cash'; import 'materialize-css/js/global'; -import 'materialize-css/js/waves' -import Component from 'exports-loader?exports=default Component!materialize-css/js/component.js' +import 'materialize-css/js/waves'; +// component.js has no exports; the Vite plugin `materialize-component-export` +// appends `export default Component;` at build time so we can import it here +// and expose it as a global for the IIFE-based materialize component files. +import Component from 'materialize-css/js/component.js'; -global.Component = Component; +globalThis.Component = Component; diff --git a/web-src/src/main-app/components/scripts/MainAppContent.vue b/web-src/src/main-app/components/scripts/MainAppContent.vue index 8d2ea649..eb97c2b5 100644 --- a/web-src/src/main-app/components/scripts/MainAppContent.vue +++ b/web-src/src/main-app/components/scripts/MainAppContent.vue @@ -70,7 +70,7 @@ export default { flex-direction: column; } -.main-app-content >>> .input-field label { +.main-app-content :deep(.input-field label) { top: 0; text-overflow: ellipsis; diff --git a/web-src/src/main-app/components/scripts/ScriptListGroup.vue b/web-src/src/main-app/components/scripts/ScriptListGroup.vue index ab9e5283..1f2b9b3a 100644 --- a/web-src/src/main-app/components/scripts/ScriptListGroup.vue +++ b/web-src/src/main-app/components/scripts/ScriptListGroup.vue @@ -48,7 +48,7 @@ export default { line-height: 16px; } -.script-list-group >>> .collection-item.script-list-item { +.script-list-group :deep(.collection-item.script-list-item) { padding-left: 36px; } diff --git a/web-src/src/main-app/components/scripts/ScriptViewScheduleHolder.vue b/web-src/src/main-app/components/scripts/ScriptViewScheduleHolder.vue index b1d8ae2a..a9a7c956 100644 --- a/web-src/src/main-app/components/scripts/ScriptViewScheduleHolder.vue +++ b/web-src/src/main-app/components/scripts/ScriptViewScheduleHolder.vue @@ -120,7 +120,7 @@ div:not(.modal) > .schedule-panel { background-color: var(--background-color-level-4dp); } -div > .schedule-panel >>> .card-action { +div > .schedule-panel :deep(.card-action) { background: none; } \ No newline at end of file diff --git a/web-src/src/main-app/components/scripts/ScriptsList.vue b/web-src/src/main-app/components/scripts/ScriptsList.vue index 70d50433..28329e20 100644 --- a/web-src/src/main-app/components/scripts/ScriptsList.vue +++ b/web-src/src/main-app/components/scripts/ScriptsList.vue @@ -1,8 +1,8 @@