Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
1e5060e
Fix shared context issues for security salts
varmar05 Jan 20, 2025
c28a14f
Address review comments
varmar05 Jan 21, 2025
94e50e6
Merge pull request #359 from MerginMaps/fix_token_reuse
MarcelGeo Jan 21, 2025
767de67
FE: impl of report exporter to settings
MarcelGeo Jan 23, 2025
2523854
BE: migration to export mergin_statistics
MarcelGeo Jan 23, 2025
c2de0f6
Unfy name of csv + unify sorting of columns in report
MarcelGeo Jan 23, 2025
4533b85
Black
MarcelGeo Jan 23, 2025
4ffa9fa
Use dataclass for callhome data
MarcelGeo Jan 24, 2025
5a30f10
Sanitize download button + typo indialog
MarcelGeo Jan 24, 2025
c2fb398
Address commnets about jsonb @varmar05
MarcelGeo Jan 24, 2025
a90504c
use date-fns for the same like new Date :)
MarcelGeo Jan 24, 2025
d429623
remove store stats on server startup
MarcelGeo Jan 24, 2025
389f0f5
Run save stats every 12 hours
MarcelGeo Jan 24, 2025
064545d
Merge pull request #363 from MerginMaps/gh#2726-callhome-stats-export
MarcelGeo Jan 24, 2025
7ba381a
Add secondary severity for buttons in overview page
MarcelGeo Jan 27, 2025
0684cbc
Added: util methods to validate names
MarcelGeo Jan 28, 2025
94e9bc7
Address comments @varmar05
MarcelGeo Jan 28, 2025
f1e7577
Clenup: print
MarcelGeo Jan 28, 2025
60f5f75
Merge pull request #366 from MerginMaps/gh#2740-name-validations
MarcelGeo Jan 28, 2025
53f57d3
Merge pull request #365 from MerginMaps/ee-overview-secondary-buttons
MarcelGeo Feb 3, 2025
e839e81
Base for init command
MarcelGeo Feb 5, 2025
e27fecf
Restrict file types and filepaths
harminius Feb 6, 2025
e9bd8f2
Improve mimetype check and add tests
harminius Feb 6, 2025
55e2707
Add filepath validation and fix test
harminius Feb 6, 2025
009f1ff
Disable sedning stats on application init
MarcelGeo Feb 7, 2025
86ac974
Validated filepath of the file to be uploaded
harminius Feb 7, 2025
d55ddf7
Cleanup
harminius Feb 7, 2025
c1afc25
Fix test logic
harminius Feb 10, 2025
23d8c73
Duplicated test
harminius Feb 10, 2025
ac51ee5
Abort if recreate prompt is "no"
MarcelGeo Feb 10, 2025
09cb239
Add test for traverse folder in the filepath
harminius Feb 10, 2025
fd70caa
Merge pull request #372 from MerginMaps/path_validation_in_upload
MarcelGeo Feb 10, 2025
eda20ee
Enhance confirm comand
MarcelGeo Feb 10, 2025
dd5e6c9
Merge pull request #369 from MerginMaps/init-command
MarcelGeo Feb 10, 2025
e68575f
Bump version 2025.2.0
MarcelGeo Feb 10, 2025
5e02281
Merge pull request #374 from MerginMaps/bump_2025.2.0
MarcelGeo Feb 10, 2025
9a8fb56
Rename editors in overview
MarcelGeo Feb 11, 2025
df9f5fc
Merge pull request #376 from MerginMaps/fix_crontab_editors
MarcelGeo Feb 11, 2025
bcb81cf
Backport error handling for username
MarcelGeo Feb 13, 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
6 changes: 6 additions & 0 deletions .prod.env
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ SECRET_KEY=fixme

#BEARER_TOKEN_EXPIRATION=3600 * 12 # in seconds

#SECURITY_BEARER_SALT=NODEFAULT
SECURITY_BEARER_SALT=fixme

#SECURITY_EMAIL_SALT=NODEFAULT
SECURITY_EMAIL_SALT=fixme

#SECURITY_PASSWORD_SALT=NODEFAULT
SECURITY_PASSWORD_SALT=fixme

Expand Down
3 changes: 3 additions & 0 deletions server/.test.env
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ GLOBAL_WORKSPACE='mergin'
GLOBAL_STORAGE=104857600
COLLECT_STATISTICS=0
GEODIFF_WORKING_DIR=/tmp/geodiff
SECURITY_BEARER_SALT='bearer'
SECURITY_EMAIL_SALT='email'
SECURITY_PASSWORD_SALT='password'
2 changes: 1 addition & 1 deletion server/Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ urllib3 = "==2.2.2"
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"
typing_extensions = "==4.12.2"
# requirements for development on windows
colorama = "==0.4.5"

Expand Down
965 changes: 492 additions & 473 deletions server/Pipfile.lock

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions server/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from mergin.sync.tasks import remove_temp_files, remove_projects_backups
from mergin.celery import celery, configure_celery
from mergin.stats.config import Configuration
from mergin.stats.tasks import send_statistics
from mergin.stats.tasks import save_statistics, send_statistics
from mergin.stats.app import register as register_stats

Configuration.SERVER_TYPE = "ce"
Expand Down Expand Up @@ -65,14 +65,14 @@ def setup_periodic_tasks(sender, **kwargs):
remove_projects_backups,
name="remove old project backups",
)
sender.add_periodic_task(
crontab(hour="*/12", minute=0),
save_statistics,
name="Save usage statistics to database",
)
if Configuration.COLLECT_STATISTICS:
sender.add_periodic_task(
crontab(hour=randint(0, 5), minute=randint(0, 60)),
send_statistics,
name="send usage statistics",
)


# send report after start
if Configuration.COLLECT_STATISTICS:
send_statistics.delay()
2 changes: 2 additions & 0 deletions server/mergin/.env
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
GEODIFF_LOGGER_LEVEL="2"
# only for dev - should be overwritten in production
SECRET_KEY='top-secret'
SECURITY_BEARER_SALT='top-secret'
SECURITY_EMAIL_SALT='top-secret'
SECURITY_PASSWORD_SALT='top-secret'
MAIL_DEFAULT_SENDER=''
FLASK_DEBUG=0
10 changes: 1 addition & 9 deletions server/mergin/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,15 +139,6 @@ def create_simple_app() -> Flask:
if Configuration.GEVENT_WORKER:
flask_app.wsgi_app = GeventTimeoutMiddleware(flask_app.wsgi_app)

@flask_app.cli.command()
def init_db():
"""Re-creates application database"""
print("Database initialization ...")
db.drop_all(bind=None)
db.create_all(bind=None)
db.session.commit()
print("Done. Tables created.")

add_commands(flask_app)

return flask_app
Expand Down Expand Up @@ -211,6 +202,7 @@ def load_user_from_header(header_val): # pylint: disable=W0613,W0612
try:
data = decode_token(
app.app.config["SECRET_KEY"],
app.app.config["SECURITY_BEARER_SALT"],
header_val,
app.app.config["BEARER_TOKEN_EXPIRATION"],
)
Expand Down
17 changes: 10 additions & 7 deletions server/mergin/auth/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,17 +80,15 @@ def authenticate(login, password):
return user


def generate_confirmation_token(app, email):
def generate_confirmation_token(app, email, salt):
serializer = URLSafeTimedSerializer(app.config["SECRET_KEY"])
return serializer.dumps(email, salt=app.config["SECURITY_PASSWORD_SALT"])
return serializer.dumps(email, salt=salt)


def confirm_token(token, expiration=3600 * 24 * 3):
def confirm_token(token, salt, expiration=3600):
serializer = URLSafeTimedSerializer(current_app.config["SECRET_KEY"])
try:
email = serializer.loads(
token, salt=current_app.config["SECURITY_PASSWORD_SALT"], max_age=expiration
)
email = serializer.loads(token, salt=salt, max_age=expiration)
except:
return
return email
Expand All @@ -103,7 +101,12 @@ def send_confirmation_email(app, user, url, template, header, **kwargs):
"""
from ..celery import send_email_async

token = generate_confirmation_token(app, user.email)
salt = (
app.config["SECURITY_EMAIL_SALT"]
if url == "confirm-email"
else app.config["SECURITY_PASSWORD_SALT"]
)
token = generate_confirmation_token(app, user.email, salt)
confirm_url = f"{url}/{token}"
html = render_template(
template, subject=header, confirm_url=confirm_url, user=user, **kwargs
Expand Down
6 changes: 2 additions & 4 deletions server/mergin/auth/bearer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
from flask.sessions import TaggedJSONSerializer


def decode_token(secret_key, token, max_age=None):
salt = "bearer-session"
def decode_token(secret_key, salt, token, max_age=None):
serializer = TaggedJSONSerializer()
signer_kwargs = {"key_derivation": "hmac", "digest_method": hashlib.sha1}
s = URLSafeTimedSerializer(
Expand All @@ -17,8 +16,7 @@ def decode_token(secret_key, token, max_age=None):
return s.loads(token, max_age=max_age)


def encode_token(secret_key, data):
salt = "bearer-session"
def encode_token(secret_key, salt, data):
serializer = TaggedJSONSerializer()
signer_kwargs = {"key_derivation": "hmac", "digest_method": hashlib.sha1}
s = URLSafeTimedSerializer(
Expand Down
2 changes: 2 additions & 0 deletions server/mergin/auth/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@


class Configuration(object):
SECURITY_BEARER_SALT = config("SECURITY_BEARER_SALT")
SECURITY_EMAIL_SALT = config("SECURITY_EMAIL_SALT")
SECURITY_PASSWORD_SALT = config("SECURITY_PASSWORD_SALT")
BEARER_TOKEN_EXPIRATION = config(
"BEARER_TOKEN_EXPIRATION", default=3600 * 12, cast=int
Expand Down
21 changes: 17 additions & 4 deletions server/mergin/auth/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
from ..sync.utils import files_size


EMAIL_CONFIRMATION_EXPIRATION = 12 * 3600


# public endpoints
def user_profile(user, return_all=True):
"""Return user profile in json format
Expand Down Expand Up @@ -143,7 +146,11 @@ def login_public(): # noqa: E501
"email": user.email,
"expire": str(expire),
}
token = encode_token(current_app.config["SECRET_KEY"], token_data)
token = encode_token(
current_app.config["SECRET_KEY"],
current_app.config["SECURITY_BEARER_SALT"],
token_data,
)

data = user_profile(user)
data["session"] = {"token": token, "expire": expire}
Expand Down Expand Up @@ -297,7 +304,7 @@ def password_reset(): # pylint: disable=W0613,W0612


def confirm_new_password(token): # pylint: disable=W0613,W0612
email = confirm_token(token)
email = confirm_token(token, salt=current_app.config["SECURITY_PASSWORD_SALT"])
if not email:
abort(400, "Invalid token")

Expand All @@ -315,7 +322,11 @@ def confirm_new_password(token): # pylint: disable=W0613,W0612


def confirm_email(token): # pylint: disable=W0613,W0612
email = confirm_token(token)
email = confirm_token(
token,
expiration=EMAIL_CONFIRMATION_EXPIRATION,
salt=current_app.config["SECURITY_EMAIL_SALT"],
)
if not email:
abort(400, "Invalid token")

Expand Down Expand Up @@ -375,7 +386,9 @@ def register_user(): # pylint: disable=W0613,W0612
if form.validate():
user = User.create(form.username.data, form.email.data, form.password.data)
user_created.send(user, source="admin")
token = generate_confirmation_token(current_app, user.email)
token = generate_confirmation_token(
current_app, user.email, current_app.config["SECURITY_EMAIL_SALT"]
)
confirm_url = f"confirm-email/{token}"
html = render_template(
"email/user_created.html",
Expand Down
25 changes: 19 additions & 6 deletions server/mergin/auth/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,26 @@


def username_validation(form, field):
from ..sync.utils import is_name_allowed
from ..sync.utils import (
has_valid_characters,
has_valid_first_character,
check_filename,
is_reserved_word,
)

if field.data and (not is_name_allowed(field.data) or "@" in field.data):
raise ValidationError(
f"Please don't start username with . and "
f"use only alphanumeric or these -._! characters in {field.name}."
)
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)


class PasswordValidator:
Expand Down
Loading
Loading