diff --git a/LICENSES/CLA-signed-list.md b/LICENSES/CLA-signed-list.md index 431143ad..1f4c1cc9 100644 --- a/LICENSES/CLA-signed-list.md +++ b/LICENSES/CLA-signed-list.md @@ -21,4 +21,5 @@ C/ My company has custom contribution contract with Lutra Consulting Ltd. or I a * luxusko, 25th August 2023 * jozef-budac, 30th January 2024 * fernandinand, 13th March 2025 -* xkello, 26th January 2026 \ No newline at end of file +* wonder-sk, 9th February 2026 +* xkello, 26th January 2026 diff --git a/README.md b/README.md index 5d3a912b..d1dcbbe0 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ Try Mergin Maps at https://merginmaps.com/ - the SaaS service run by Lutra Consu ### Running locally -A step-by-step guide how to run local Mergin Maps instance can be found in our [documentation](https://merginmaps.com/docs/dev/mergince/). +A step-by-step guide how to run local Mergin Maps instance can be found in our [documentation](https://merginmaps.com/docs/server/install/). ### Manage Mergin Maps diff --git a/deployment/enterprise/README.md b/deployment/enterprise/README.md index 7d93ce42..05485d71 100644 --- a/deployment/enterprise/README.md +++ b/deployment/enterprise/README.md @@ -3,22 +3,17 @@ Suitable for Ubuntu servers, single-node deployment using Docker Compose and system NGINX as a reverse proxy. > [!IMPORTANT] -> Docker images for Mergin Maps Enterprise Edition are stored in a private AWS ECR repository. +> Docker images for Mergin Maps Enterprise Edition are stored in private Dockerhub repositories. > To access them, you need a Mergin Maps Enterprise [subscription](https://merginmaps.com/pricing). > Please contact the Mergin Maps [sales team](https://merginmaps.com/contact-sales)! -## Login to Mergin Maps AWS ECR Repository - -```shell -aws ecr --region eu-west-1 get-login-password | docker login --username AWS --password-stdin 433835555346.dkr.ecr.eu-west-1.amazonaws.com -``` - ## Load Docker Images, Configure, and Run Mergin Maps Stack -To run Mergin Maps, you need to load local Docker images (if any). Make sure you have access to Lutra's ECR repository. You can check this by running: +Login to dockerhub (you should have already received your access token from Mergin Maps team). +To run Mergin Maps, you need to load local Docker images (if any). Make sure you have access to Lutra's repositories. You can check this by running: ```shell -docker pull 433835555346.dkr.ecr.eu-west-1.amazonaws.com/mergin/mergin-ee-back:2025.3.0 +docker pull lutraconsulting/merginmaps-backend-ee:2025.7.3 ``` Then modify the [docker-compose file](docker-compose.yml) and create the environment file `.prod.env` from `.env.template`. Details about configuration can be found in the [docs](https://merginmaps.com/docs/server/install/). diff --git a/deployment/enterprise/docker-compose.maps.yml b/deployment/enterprise/docker-compose.maps.yml index 47d8cdc0..677a07ca 100644 --- a/deployment/enterprise/docker-compose.maps.yml +++ b/deployment/enterprise/docker-compose.maps.yml @@ -6,7 +6,7 @@ networks: services: qgis: container_name: mergin-qgis - image: 433835555346.dkr.ecr.eu-west-1.amazonaws.com/mergin/qgis-server-ee:2025.1.0 + image: lutraconsulting/qgis-server:2025.1.0 user: 1000:999 networks: - mergin-net @@ -30,7 +30,7 @@ services: - ./qgis_nginx.conf:/etc/nginx/conf.d/default.conf qgis_extractor: container_name: mergin-qgis-extractor - image: 433835555346.dkr.ecr.eu-west-1.amazonaws.com/mergin/qgis-extractor-ee:2025.3.0 + image: lutraconsulting/qgis-extractor:2025.3.1 user: 901:999 networks: - mergin-net diff --git a/deployment/enterprise/docker-compose.yml b/deployment/enterprise/docker-compose.yml index a9f33023..7381b904 100644 --- a/deployment/enterprise/docker-compose.yml +++ b/deployment/enterprise/docker-compose.yml @@ -5,7 +5,7 @@ networks: services: server: - image: 433835555346.dkr.ecr.eu-west-1.amazonaws.com/mergin/mergin-ee-back:2025.7.3 + image: lutraconsulting/merginmaps-backend-ee:2025.7.3 container_name: merginmaps-server restart: always user: 901:999 @@ -22,7 +22,7 @@ services: networks: - mergin web: - image: 433835555346.dkr.ecr.eu-west-1.amazonaws.com/mergin/mergin-ee-front:2025.7.3 + image: lutraconsulting/merginmaps-frontend-ee:2025.7.3 container_name: merginmaps-web restart: always depends_on: @@ -52,7 +52,7 @@ services: - server celery-beat: - image: 433835555346.dkr.ecr.eu-west-1.amazonaws.com/mergin/mergin-ee-back:2025.7.3 + image: lutraconsulting/merginmaps-backend-ee:2025.7.3 container_name: merginmaps-celery-beat restart: always user: 901:999 @@ -66,7 +66,7 @@ services: - mergin celery-worker: - image: 433835555346.dkr.ecr.eu-west-1.amazonaws.com/mergin/mergin-ee-back:2025.7.3 + image: lutraconsulting/merginmaps-backend-ee:2025.7.3 container_name: merginmaps-celery-worker restart: always user: 901:999 diff --git a/server/mergin/sync/config.py b/server/mergin/sync/config.py index b6ec79f7..3360a627 100644 --- a/server/mergin/sync/config.py +++ b/server/mergin/sync/config.py @@ -78,5 +78,7 @@ class Configuration(object): EXCLUDED_CLONE_FILENAMES = config( "EXCLUDED_CLONE_FILENAMES", default="qgis_cfg.xml", cast=Csv() ) + # files that should be ignored during extension and MIME type checks + UPLOAD_FILES_WHITELIST = config("UPLOAD_FILES_WHITELIST", default="", cast=Csv()) # max batch size for fetch projects in batch endpoint MAX_BATCH_SIZE = config("MAX_BATCH_SIZE", default=100, cast=int) diff --git a/server/mergin/sync/utils.py b/server/mergin/sync/utils.py index de0fbe94..dd2d6a2c 100644 --- a/server/mergin/sync/utils.py +++ b/server/mergin/sync/utils.py @@ -27,6 +27,8 @@ from flask import current_app from pathlib import Path +from .config import Configuration + def generate_checksum(file, chunk_size=4096): """ @@ -349,6 +351,8 @@ def has_trailing_space(filepath: str) -> bool: def is_supported_extension(filepath) -> bool: """Check whether file's extension is supported.""" + if check_skip_validation(filepath): + return True ext = os.path.splitext(filepath)[1].lower() return ext and ext not in FORBIDDEN_EXTENSIONS @@ -491,6 +495,15 @@ def is_supported_extension(filepath) -> bool: ".xnk", } + +def check_skip_validation(file_path: str) -> bool: + """ + Check if we can skip validation for this file path. + Some files are allowed even if they have forbidden extension or mime type. + """ + return file_path in Configuration.UPLOAD_FILES_WHITELIST + + FORBIDDEN_MIME_TYPES = { "application/x-msdownload", "application/x-sh", @@ -515,6 +528,8 @@ def is_supported_extension(filepath) -> bool: def is_supported_type(filepath) -> bool: """Check whether the file mimetype is supported.""" + if check_skip_validation(filepath): + return True mime_type = get_mimetype(filepath) return mime_type.startswith("image/") or mime_type not in FORBIDDEN_MIME_TYPES diff --git a/server/mergin/tests/test_utils.py b/server/mergin/tests/test_utils.py index 00b3e1c6..410c902a 100644 --- a/server/mergin/tests/test_utils.py +++ b/server/mergin/tests/test_utils.py @@ -22,10 +22,13 @@ has_valid_characters, has_valid_first_character, check_filename, + is_supported_extension, + is_supported_type, is_valid_path, get_x_accel_uri, wkb2wkt, has_trailing_space, + check_skip_validation, ) from ..auth.models import LoginHistory, User from . import json_headers @@ -322,3 +325,46 @@ class TestSchema(Schema): "size": "disk_usage", } assert schema_map == expected_map + + +def test_check_skip_validation(): + ALLOWED_FILES = ["script.js", "config/script.js"] + + # We patch the Configuration class attribute directly + with patch("mergin.sync.utils.Configuration.UPLOAD_FILES_WHITELIST", ALLOWED_FILES): + + # Test allowed files + for file_path in ALLOWED_FILES: + assert check_skip_validation(file_path) + + # Test not allowed files + assert not check_skip_validation("test.py") + assert not check_skip_validation("/some/path/test.py") + assert not check_skip_validation("image.png") + + +def test_is_supported_extension(): + ALLOWED_FILES = ["script.js", "config/script.js"] + + with patch("mergin.sync.utils.Configuration.UPLOAD_FILES_WHITELIST", ALLOWED_FILES): + for file_path in ALLOWED_FILES: + assert is_supported_extension(file_path) + + # Allowed normal file + assert is_supported_extension("image.png") + + # Forbidden file + assert not is_supported_extension("test.js") + + +def test_mime_type_validation_skip(): + ALLOWED_FILES = ["script.js", "config/script.js"] + # Mocking get_mimetype to return forbidden mime type + with patch( + "mergin.sync.utils.get_mimetype", return_value="application/x-python-code" + ), patch("mergin.sync.utils.Configuration.UPLOAD_FILES_WHITELIST", ALLOWED_FILES): + for file_path in ALLOWED_FILES: + assert is_supported_type(file_path) + + # Should be forbidden + assert not is_supported_type("other.js")