Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
9a78360
Enhance project_name_validation
MarcelGeo Feb 7, 2025
8bac3a7
Remove username from /app/admin/user
MarcelGeo Feb 7, 2025
fb2b9d6
Split path manipulation and file type validation
harminius Feb 7, 2025
6d2c22b
Switch to blacklists
harminius Feb 10, 2025
189c952
reformat
harminius Feb 10, 2025
67ac1ee
Move filetype check
harminius Feb 10, 2025
05189b1
residue
harminius Feb 10, 2025
4c3c647
Add magic lib
harminius Feb 10, 2025
f15f47f
Merge branch 'develop' into gh#2751-enh-validation
MarcelGeo Feb 11, 2025
bd8c208
Merge pull request #373 from MerginMaps/gh#2751-enh-validation
MarcelGeo Feb 11, 2025
dfb99bc
Update abort messages to be more helpful
harminius Feb 13, 2025
786904b
Merge pull request #370 from MerginMaps/file_upload_restriction
harminius Feb 19, 2025
1896274
TODO: Revisit schemas
harminius Feb 19, 2025
54344c3
Added abilty to store breadcrumbs in store, not just from router
MarcelGeo Feb 19, 2025
b7018f4
Drop error details index in sync failures table
harminius Feb 20, 2025
a09da72
black
harminius Feb 20, 2025
848e597
Drop index for the model
harminius Feb 20, 2025
6c137f7
Merge pull request #379 from MerginMaps/drop_error_details_index_gh#2776
harminius Feb 20, 2025
ced4b2a
Merge pull request #378 from MerginMaps/fix-breadcrumps
MarcelGeo Feb 20, 2025
ffb5fc7
Add missing libmagic1 to dockerfile
varmar05 Feb 21, 2025
66c2938
Merge pull request #380 from MerginMaps/fix_build
MarcelGeo Feb 21, 2025
854d98b
Fix message in server check
MarcelGeo Feb 24, 2025
9015c0f
Merge pull request #381 from MerginMaps/fix-message-server-check
MarcelGeo Feb 24, 2025
4a0659d
Migrate FE nginx to use port 8080 transparently
varmar05 Feb 24, 2025
6d90ce7
Merge pull request #382 from MerginMaps/fix_proxy_unpriviledged
MarcelGeo Feb 24, 2025
7ca5200
Adjust length of username to some sensible length
MarcelGeo Feb 25, 2025
ea5e028
Add tests for usernames
MarcelGeo Feb 25, 2025
ba1f0a1
Add additioanl checks for reserved words
MarcelGeo Feb 26, 2025
df31432
Merge pull request #383 from MerginMaps/fix_username_validation
MarcelGeo Feb 26, 2025
d80814d
Allow admin@example.com
MarcelGeo Feb 26, 2025
35bde8b
Merge remote-tracking branch 'origin/develop' into fix-tests-admin-is…
MarcelGeo Feb 26, 2025
db495d6
Merge pull request #384 from MerginMaps/fix-tests-admin-is-now-ok-email
MarcelGeo Feb 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion development.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Watching the type definitions is also useful to pick up any changes to imports o

## Running locally in a docker composition

If you want to run the whole stack locally, you can use the docker. Docker will build the images from yout local files and run the services.
If you want to run the whole stack locally, you can use the docker. Docker will build the images from your local files and run the services.

```shell
# Run the docker composition with the current Dockerfiles
Expand Down
3 changes: 2 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,13 @@ services:
restart: always
depends_on:
- server-gunicorn
user: 101:999
links:
- db
networks:
- merginmaps
proxy:
image: nginxinc/nginx-unprivileged:1.25.5
image: nginxinc/nginx-unprivileged:1.27
container_name: merginmaps-proxy
restart: always
# run nginx as built-in user but with group mergin-family for files permissions
Expand Down
2 changes: 1 addition & 1 deletion nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ server {
# we don't want nginx trying to do something clever with
# redirects, we set the Host: header above already.
proxy_redirect off;
proxy_pass http://merginmaps-web;
proxy_pass http://merginmaps-web:8080;
}

# proxy to backend
Expand Down
2 changes: 1 addition & 1 deletion server/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ RUN apt-get update -y && \
python3-pip \
python3-setuptools \
iputils-ping \
gcc build-essential binutils cmake extra-cmake-modules libsqlite3-mod-spatialite && \
gcc build-essential binutils cmake extra-cmake-modules libsqlite3-mod-spatialite libmagic1 && \
rm -rf /var/lib/apt/lists/*


Expand Down
1 change: 1 addition & 0 deletions server/Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ shapely = "==2.0.6"
psycogreen = "==1.0.2"
importlib-metadata = "==8.4.0" # https://github.com/pallets/flask/issues/4502
typing_extensions = "==4.12.2"
python-magic = "==0.4.27"
# requirements for development on windows
colorama = "==0.4.5"

Expand Down
132 changes: 68 additions & 64 deletions server/Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 1 addition & 4 deletions server/mergin/auth/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -201,14 +201,10 @@ paths:
schema:
type: object
required:
- username
- email
- password
- confirm
properties:
username:
type: string
example: john.doe
email:
type: string
format: email
Expand Down Expand Up @@ -686,6 +682,7 @@ paths:
description: User info
content:
application/json:
# TODO: fix this to match the ma.SQLAlchemy schema or use UserDetail schema
schema:
$ref: "#/components/schemas/UserInfo"
"401":
Expand Down
3 changes: 2 additions & 1 deletion server/mergin/auth/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,8 @@ def register_user(): # pylint: disable=W0613,W0612
from ..celery import send_email_async

form = UserRegistrationForm()
if form.validate():
form.username.data = User.generate_username(form.email.data)
if form.validate_on_submit():
user = User.create(form.username.data, form.email.data, form.password.data)
user_created.send(user, source="admin")
token = generate_confirmation_token(
Expand Down
16 changes: 10 additions & 6 deletions server/mergin/auth/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,16 @@ def username_validation(form, field):
is_reserved_word,
)

errors = [
has_valid_characters(field.data),
has_valid_first_character(field.data),
is_reserved_word(field.data),
check_filename(field.data),
]
errors = (
[
has_valid_characters(field.data),
has_valid_first_character(field.data),
is_reserved_word(field.data),
check_filename(field.data),
]
if field.data
else []
)
for error in errors:
if error:
raise ValidationError(error)
Expand Down
6 changes: 4 additions & 2 deletions server/mergin/auth/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from ..app import db
from ..sync.models import ProjectUser
from ..sync.utils import get_user_agent, get_ip, get_device_id
from ..sync.utils import get_user_agent, get_ip, get_device_id, is_reserved_word


class User(db.Model):
Expand Down Expand Up @@ -200,7 +200,9 @@ def generate_username(cls, email: str) -> Optional[str]:
# remove forbidden chars
username = re.sub(
r"[\@\#\$\%\^\&\*\(\)\{\}\[\]\?\'\"`,;\:\+\=\~\\\/\|\<\>]", "", username
)
).ljust(4, "0")
# additional check for reserved words
username = f"{username}0" if is_reserved_word(username) else username
# check if we already do not have existing usernames
suffix = db.session.execute(
text(
Expand Down
2 changes: 1 addition & 1 deletion server/mergin/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def _check_server(): # pylint: disable=W0612
"No contact email set. Please set CONTACT_EMAIL environment variable",
)
else:
click.secho(f"Base URL of server is {base_url}", fg="green")
click.secho(f"Your contact email is {contact_email}.", fg="green")

tables = db.engine.table_names()
if not tables:
Expand Down
3 changes: 2 additions & 1 deletion server/mergin/sync/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@

def project_name_validation(name: str) -> str | None:
"""Check whether project name is valid"""
if not name.strip():
name = name.strip() if name is not None else name
if not name:
return "Project name cannot be empty"
errors = [
has_valid_characters(name),
Expand Down
2 changes: 1 addition & 1 deletion server/mergin/sync/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1121,7 +1121,7 @@ class SyncFailuresHistory(db.Model):
error_type = db.Column(
db.String, index=True
) # e.g. push_start, push_finish, push_lost
error_details = db.Column(db.String, index=True)
error_details = db.Column(db.String)
timestamp = db.Column(db.DateTime(), default=datetime.utcnow, index=True)
user_id = db.Column(
db.Integer, db.ForeignKey("user.id", ondelete="SET NULL"), nullable=True
Expand Down
21 changes: 18 additions & 3 deletions server/mergin/sync/public_api_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
import binascii
import functools
import json
import mimetypes
import os
import logging
from dataclasses import asdict
from typing import Dict
from urllib.parse import quote
import uuid
from datetime import datetime

import psycopg2
from blinker import signal
from connexion import NoContent, request
Expand Down Expand Up @@ -87,6 +87,9 @@
get_project_path,
get_device_id,
is_valid_path,
is_supported_type,
is_supported_extension,
get_mimetype,
)
from .errors import StorageLimitHit
from ..utils import format_time_delta
Expand Down Expand Up @@ -406,7 +409,7 @@ def download_project_file(
if not is_binary(abs_path):
mime_type = "text/plain"
else:
mime_type = mimetypes.guess_type(abs_path)[0]
mime_type = get_mimetype(abs_path)
resp.headers["Content-Type"] = mime_type
resp.headers["Content-Disposition"] = "attachment; filename={}".format(
quote(os.path.basename(file).encode("utf-8"))
Expand Down Expand Up @@ -813,7 +816,16 @@ def project_push(namespace, project_name):
if not all(ele.path != item.path for ele in project.files):
abort(400, f"File {item.path} has been already uploaded")
if not is_valid_path(item.path):
abort(400, f"File {item.path} contains invalid characters.")
abort(
400,
f"Unsupported file name detected: {item.path}. Please remove the invalid characters.",
)
if not is_supported_extension(item.path):
abort(
400,
f"Unsupported file type detected: {item.path}. "
f"Please remove the file or try compressing it into a ZIP file before uploading",
)

# changes' files must be unique
changes_files = [
Expand Down Expand Up @@ -1042,6 +1054,9 @@ def push_finish(transaction_id):
)
corrupted_files.append(f.path)
continue
if not is_supported_type(dest_file):
logging.info(f"Rejecting blacklisted file: {dest_file}")
abort(400, f"Unsupported file type detected: {f.path}")

if expected_size != os.path.getsize(dest_file):
logging.error(
Expand Down
Loading
Loading