Skip to content
3 changes: 2 additions & 1 deletion LICENSES/CLA-signed-list.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
* wonder-sk, 9th February 2026
* xkello, 26th January 2026
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 4 additions & 9 deletions deployment/enterprise/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/).
Expand Down
4 changes: 2 additions & 2 deletions deployment/enterprise/docker-compose.maps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
8 changes: 4 additions & 4 deletions deployment/enterprise/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions server/mergin/sync/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
15 changes: 15 additions & 0 deletions server/mergin/sync/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
from flask import current_app
from pathlib import Path

from .config import Configuration


def generate_checksum(file, chunk_size=4096):
"""
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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",
Expand All @@ -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

Expand Down
46 changes: 46 additions & 0 deletions server/mergin/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Loading